In this post, we will be creating a survey website with the help of Remix framework and KendoReact. Learn how to speed up app development and offer amazing UI and UX.
The Remix Framework is a great server and browser runtime that provides quick page loads and swift transitions by utilizing distributed systems and native browser features instead of old-fashioned static builds. Built on the Fetch API rather than the Node server, it can run anywhere; currently, it also runs natively on Cloudflare Workers and supports serverless and traditional Node.js environments.
Fast page-load time is only a part of our true goal—we’re also after better user experiences. The Remix framework is here to serve you from the first request to the flashiest UX your designers can create. In this blog, we’ll go over a few explanations and code examples to see what Remix is all about, and, after that, we will build a React survey site using the Remix framework and KendoReact.
KendoReact is a React component library that makes designing and building powerful apps much faster. It is one of the Kendo UI libraries available for JavaScript frameworks—the others are for Angular, Vue and jQuery.
To create a Remix project, enter the following code into your terminal:
npx create-remix@latest
# choose Remix App Server
cd [project name]
npm run dev
Open your browser and enter this link: https://localhost:3000. You should see the Remix app up and running. Let’s go back to the code editor to familiarize ourselves with the folder structures and their purpose.
In the root.jsx file, you’ll see a default app function exported with the following code.
export default function App() {
return (
<Document>
<Layout>
<Outlet />
</Layout>
</Document>
);
}
In the above code, the function app returns a component tree that consists of the Document
, Layout
and Outlet
components, but what does this all mean? Before explaining that, let’s look at another function (Layout
function) in the root.jsx
file, because this is where all the JSX routes located in our /app/routes
are rendered.
function Layout({ children }) {
return (
<div className="remix-app">
<header className="remix-app__header">
<div className="container remix-app__header-content">
<Link to="/" title="Remix" className="remix-app__header-home-link">
Portfolio Website
</Link>
</div>
</header>
<div className="remix-app__main">
<div className="container remix-app__main-content">{children}</div>
</div>
<footer className="remix-app__footer">
<div className="container remix-app__footer-content">
<p>Porfolio website made with remix and kendo ui</p>
</div>
</footer>
</div>
);
}
The function Layout
receives the components from the app directory as props.children
via the Outlet
component nested in it. Then it renders the child component (Outlet
) as shown below.
<div className="container remix-app__main-content">{children}</div>
That is then called into the function App
to render the views. Also, the page navigation concept used in Remix is similar to that of Next.js, where every file created in the folder /app/routes
is its page or routes.
Data loading is built into Remix.
Note: To test the code below, you could paste it into the
index.jsx
file or create a new route by creating atest.jsx
file in thedirectory /app/routes
and entering this URL—www.localhost:3000/test—in your browser.
If your web dev experience began in the last few years, you’re probably used to creating two things here: an API route for data requests and a frontend component that consumes it. In Remix, your frontend component communicates with the loader function, which then returns the data to be consumed and a useLoaderData
hook, which the frontend component will use to consume the data returned by the loader function.
You can think of your Remix routes as backend views using React for templating—they know how to use the browser to add some elegance efficiently. Here’s a quick code view of the Remix data loading functionality using the useLoaderData
hook.
import { useLoaderData } from "remix";
export let loader = () => {
return [
{
slug: "my-first-post",
title: "My First Post"
},
{
slug: "90s-mixtape",
title: "A Mixtape I Made Just For You"
}
];
};
export default function Posts() {
let posts = useLoaderData();
console.log(posts);
return (
<div>
<h1>Posts</h1>
</div>
);
}
The loader function is the backend API for their component, and it is connected to the component for you through the useLoaderData
hook. There is a somewhat blurry line in the Remix route between the client and the server. If you try logging the post data, it will appear on both your server and browser consoles; this is because Remix is added in the client, but it is also rendered on the server to send an HTML document like a typical web framework.
Next, let’s pull data from a data source instead of the predefined data we used in the loading data section.
In real-world applications, some form of data persistence mechanism (database) is required. We’ll be using a database (SQLite) that supports Prisma, a database ORM. It’s also a great place to start if you’re unsure what database to use. Here we will focus on the Remix implementation; we will go over only the setup on the Remix app.
Two packages will be needed to get started: Prisma for database interaction and Schema for @prisma/client
development, used to make queries to our database during runtime. Enter the following command in the terminal to install the Prisma packages:
npm install --save-dev prisma
npm install @prisma/client
Next, use the following command to initialize Prisma with SQLite.
npx prisma init --datasource-provider sqlite
The above command should produce the following output:
✔ Your Prisma schema was created at prisma/schema.prisma
You can now open it in your favorite editor.
warn You already have a .gitignore. Don't forget to exclude .env to not commit any secret.
Next steps:
1. Set the DATABASE_URL in the .env file to point to your existing database. If your database has no tables yet, read https://pris.ly/d/getting-started
2. Run prisma db pull to turn your database schema into a Prisma schema.
3. Run prisma generate to generate the Prisma Client. You can then start querying your database.
More information in our documentation:
https://pris.ly/d/getting-started
You can read more about the Prisma schema from their docs for more information. Now that we’ve gotten Prisma ready, we can begin modeling our app data.
In the directory /prisma
create a prisma.schema
and add the code below.
// This is your Prisma schema file,
// learn more about it in the docs: https://pris.ly/d/prisma-schema
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "sqlite"
url = env("DATABASE_URL")
}
model Joke {
id String @id @default(uuid())
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
name String
content String
}
The generator client
shown above states that the client should be generated based on the defined model. The datasource db
states in detail which data source Prisma should connect to. Joke
is a model of the data to be stored in the database.
With that in place, run the following command. I will explain what it does in a minute.
npx prisma db push
The above command should produce the following output:
Environment variables loaded from .env
Prisma schema loaded from prisma/schema.prisma
Datasource "db": SQLite database "dev.db" at "file:./dev.db"
🚀 Your database is now in sync with your schema. Done in 194ms
✔ Generated Prisma Client (3.5.0) to ./node_modules/
@prisma/client in 26ms
The command did a few things. Firstly, our database file is created in prisma/dev.db
. Then all the necessary changes are pushed to our database to match the schema we provided. Finally, it generated Prisma’s JavaScript types, so we’ll get stellar autocomplete and type checking as we use its API for interacting with our database.
In our .gitignore
, let’s add that prisma/dev.db
, so we don’t add it to our commit on our repository. Also, we’ll want to add the .env
file to the .gitignore
so we don’t commit our secrets.
node_modules
/.cache
/build
/public/build
/prisma/dev.db
.env
Next, we will add a few lines of code into a new file that we will create, which will “seed” our database with the test data. Again, this is to bring you up to speed on using a database (SQLite) with Remix.
Create a new file called prisma/seed.ts
and copy the code below into it.
import { PrismaClient } from "@prisma/client";
let db = new PrismaClient();
async function seed() {
await Promise.all(
getJokes().map(joke => {
return db.joke.create({ data: joke });
})
);
}
seed();
function getJokes() {
// shout-out to https://icanhazdadjoke.com/
return [
{
name: "Road worker",
content: `I never wanted to believe that my Dad was stealing from his job as a road worker. But when I got home, all the signs were there.`
},
{
name: "Frisbee",
content: `I was wondering why the frisbee was getting bigger, then it hit me.`
},
{
name: "Trees",
content: `Why do trees seem suspicious on sunny days? Dunno, they're just a bit shady.`
},
{
name: "Skeletons",
content: `Why don't skeletons ride roller coasters? They don't have the stomach for it.`
},
{
name: "Hippos",
content: `Why don't you find hippopotamuses hiding in trees? They're really good at it.`
},
{
name: "Dinner",
content: `What did one plate say to the other plate? Dinner is on me!`
},
{
name: "Elevator",
content: `My first time using an elevator was an uplifting experience. The second time let me down.`
}
];
}
Feel free to add your data if you like. Now we need to run this file. We wrote it in TypeScript to get type safety (this is much more useful as our app and data models grow in complexity). So we’ll need a way to run it.
Enter the following command into the terminal to install esbuild-registe
r as a dev dependency:
npm install --save-dev esbuild-register
Now, we can run our seed.js
file with esbuild-register
.
node --require esbuild-register prisma/seed.js
Now our database has that data in it. But we don’t always want to have to remember to run that script any time I reset the database. Luckily, we don’t have to. Add this to your package.json
file:
// ...
"prisma": {
"seed": "node --require esbuild-register prisma/seed.ts"
},
"scripts": {
// ...
Now, whenever the database gets reset, Prisma will call our seeding file.
Now we need to connect the database to our app. We do this at the top of the prisma/seed.ts
file:
import { PrismaClient } from "@prisma/client";
let db = new PrismaClient();
That works just fine, but then the problem is, during development, we don’t want to close and restart our server every time we make a server-side change. So we’ve got some extra work to do to resolve this development time problem.
Note: The problem can be found in other aspects aside from Remix—any time you have a live reload of the server, you’re going to have to either disconnect and reconnect to databases (which can be slow) or do the workaround I’m about to show you.
Create a new file app/utils/db.server.ts
and paste the following code into it.
import { PrismaClient } from "@prisma/client";
let db: PrismaClient;
declare global {
var __db: PrismaClient | undefined;
}
// this is needed because in development we don't want to restart
// the server with every change, but we want to make sure we don't
// create a new connection to the DB with every change either.
if (process.env.NODE_ENV === "production") {
db = new PrismaClient();
db.$connect();
} else {
if (!global.__db) {
global.__db = new PrismaClient();
global.__db.$connect();
}
db = global.__db;
}
export { db };
The server-aspect of the filename notifies Remix that this code should never be shown in the browser. Though this is optional because Remix does an excellent job of ensuring server code doesn’t show up on the client-side of the application, in some cases, the server-only dependencies are hard to tree shake. Adding the .server to the filename hints to the compiler not to worry about the module or its imports when bundling for the browser. The .server
creates some boundaries for the compiler.
Now that the database is fully connected to our Remix application, we can begin performing CRUD operations on our application.
Now let’s make a route to view the post. We want these URLs to work. Create a dynamic route at app/routes/posts/$slug.jsx
, and then paste the following code into it.
export default function PostSlug() {
return (
<div>
<h1>Some Post</h1>
</div>
);
}
Now, if you click one of posts you should see the new page. Add the following code; it adds the loader to access the params.
import { useLoaderData } from "remix";
export let loader = async ({ params }) => {
return params.slug;
};
export default function PostSlug() {
let slug = useLoaderData();
return (
<div>
<h1>Some Post: {slug}</h1>
</div>
);
}
Note: The filename attached to the $ on the URL becomes a named key on the params.
Now that we are done going through a few core concepts of Remix, let’s build our project.
Since our Remix application is already set up, we need to add the necessary KendoReact package and its dependencies, which we will be using to build the UI of the site.
Note: KendoReact is a commercial UI component library, and as a part of this you will need to provide a license key when you use the components in your React projects. You can snag a license key through a free trial or by owning a commercial license. For more information, you can head over to the KendoReact Licensing page.
npm install --save @progress/kendo-theme-default --save @progress/kendo-react-form --save @progress/kendo-react-inputs --save @progress/kendo-react-labels --save @progress/kendo-react-buttons @progress/kendo-licensing @progress/kendo-react-intl
After successfully installing the KendoReact library, we can start developing the React app.
Let’s import the CSS file provided by KendoReact into our root.jsx
file.
import kendoUi from "@progress/kendo-theme-default/dist/all.css";
We are using a named export, which is different from how CSS files are imported in vanilla React.
To have a broader view of what’s going on, let’s navigate to the link function in the root.jsx
file and add the following objects to the array returned from the link function. The name assigned to export, which we talked about earlier, is the value for the property href
, and it’s mandatory as it is the design architecture on which Remix is built.
{ rel: "stylesheet", href: kendoUi },
Now, let’s add the following CSS code to the global.css
file located in the directory /app/styles
.
:root {
--color-foreground: hsl(0, 0%, 7%);
--color-background: hsl(0, 0%, 100%);
--color-links: hsl(213, 100%, 52%);
--color-links-hover: hsl(213, 100%, 43%);
--color-border: hsl(0, 0%, 82%);
--font-body: -apple-system, "Segoe UI", Helvetica Neue, Helvetica, Roboto,
Arial, sans-serif, system-ui, "Apple Color Emoji", "Segoe UI Emoji";
}
html {
box-sizing: border-box;
}
*,
*::before,
*::after {
box-sizing: inherit;
}
:-moz-focusring {
outline: auto;
}
:focus {
outline: var(--color-links) solid 2px;
outline-offset: 2px;
}
html,
body {
padding: 0;
margin: 0;
background-color: var(--color-background);
color: var(--color-foreground);
}
body {
font-family: var(--font-body);
line-height: 1.5;
}
a {
color: var(--color-links);
text-decoration: none;
}
a:hover {
color: var(--color-links-hover);
text-decoration: underline;
}
hr {
display: block;
height: 1px;
border: 0;
background-color: var(--color-border);
margin-top: 2rem;
margin-bottom: 2rem;
}
input:where([type="text"]),
input:where([type="search"]) {
display: block;
border: 1px solid var(--color-border);
width: 100%;
font: inherit;
line-height: 1;
height: calc(1ch + 1.5em);
padding-right: 0.5em;
padding-left: 0.5em;
color: var(--color-foreground);
}
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border-width: 0;
}
.container {
--gutter: 16px;
width: 1024px;
max-width: calc(100% - var(--gutter) * 2);
margin-right: auto;
margin-left: auto;
}
/*
* You probably want to just delete this file; it's just for the demo pages.
*/
.remix-app {
display: flex;
flex-direction: column;
min-height: 100vh;
min-height: calc(100vh - env(safe-area-inset-bottom));
}
.remix-app > * {
width: 100%;
}
.remix-app__header {
padding-top: 1rem;
padding-bottom: 1rem;
border-bottom: 1px solid var(--color-border);
}
.remix-app__header-content {
display: flex;
justify-content: space-between;
align-items: center;
}
.remix-app__header-home-link {
width: 406px;
height: 30px;
color: var(--color-foreground);
font-weight: 500;
font-size: 1.5rem;
}
.remix-app__header-nav ul {
list-style: none;
margin: 0;
display: flex;
align-items: center;
gap: 1.5em;
}
.remix-app__header-nav li {
cursor: pointer;
font-weight: bold;
}
.remix-app__main {
flex: 1 1 100%;
}
.remix-app__footer {
margin-top: 4em;
padding-top: 1rem;
padding-bottom: 1rem;
text-align: center;
border-top: 1px solid var(--color-border);
}
.remix-app__footer-content {
display: flex;
justify-content: center;
align-items: center;
}
.remix__page {
--gap: 1rem;
--space: 2rem;
display: grid;
grid-auto-rows: min-content;
gap: var(--gap);
padding-top: var(--space);
padding-bottom: var(--space);
}
@media print, screen and (min-width: 640px) {
.remix__page {
--gap: 2rem;
grid-auto-rows: unset;
grid-template-columns: repeat(2, 1fr);
}
}
@media screen and (min-width: 1024px) {
.remix__page {
--gap: 4rem;
}
}
.remix__page > main > :first-child {
margin-top: 0;
}
.remix__page > main > :last-child {
margin-bottom: 0;
}
.remix__page > aside {
margin: 0;
padding: 1.5ch 2ch;
border: solid 1px var(--color-border);
border-radius: 0.5rem;
}
.remix__page > aside > :first-child {
margin-top: 0;
}
.remix__page > aside > :last-child {
margin-bottom: 0;
}
.remix__form {
display: flex;
flex-direction: column;
gap: 1rem;
padding: 1rem;
border: 1px solid var(--color-border);
border-radius: 0.5rem;
}
.remix__form > * {
margin-top: 0;
margin-bottom: 0;
}
Note: The CSS code here is for aesthetic purposes only.
Now, let’s start by importing the necessary libraries into the project. Paste the code shown below into the index.jsx
file located in /app/routes
.
import * as React from "react";
import {
Form,
Field,
FormElement,
FieldWrapper,
} from "@progress/kendo-react-form";
import { Input } from "@progress/kendo-react-inputs";
import { Button } from "@progress/kendo-react-buttons";
import { Label, Hint, Error } from "@progress/kendo-react-labels";
...
Note: The three dots denote that the following code goes underneath.
const FormInput = (fieldRenderProps) => {
const { label, id, valid, disabled, type, max, value, ...others } =
fieldRenderProps;
return (
<FieldWrapper>
<Label editorId={id} editorValid={valid} editorDisabled={disabled}>
{label}
</Label>
<div className={"k-form-field-wrap"}>
<Input
valid={valid}
type={type}
id={id}
disabled={disabled}
maxlength={max}
{...others}
/>
</div>
</FieldWrapper>
);
};
...
We created a function FormInput
that receives an object fieldRenderProp
s in the code above. The fieldRenderProps
object keys are destructured and passed into Label
and Input
, enabling KendoReact to render the Label
and Input
based on these defined props values.
Now, let’s go over the props and understand their purpose. First, the editorId
, editorValid
and editorDisabled
props are used in identifying, validating and disabling the component Label. In contrast, Label is the content we want to render on the screen.
Next, the Input component uses valid
, type
, id
, disabled
and maxlength
, for validating, type checking, identifying, disabling and clearing the input values. The field wrapper is used for rendering props.children
.
export let loader = () => {
let data = [
{ question: "what skills do have?", answer: "" },
{ question: "how do plan on improving these skills", answer: "" },
{
question: "what platform do you intend on using for skill acquisation?",
answer: "",
},
{ question: "Are this plaforms free or paid for?", answer: "" },
{ question: "Is the platform free", answer: "" },
{
question: "what is the cost? type N/A if previous answer is free?",
answer: "",
},
];
...
Now, before passing in the function FormInput
into the KendoReact Form
component, let’s create our question data in the function loader
and return it to enable Remix to load the data for us in the Index
function we will visit later.
let question = useLoaderData();
const [counter, setCounter] = React.useState(0);
const handleSubmit = (data) => {
questions[counter].answer = data.Answer;
if (counter === questions.length - 1) {
setCounter(0);
} else {
setCounter((state) => {
return state + 1;
});
}
console.log(questions, counter);
};
...
In the code above, we created a state counter that we used to iterate the counter value, allowing the question to change each time the button is clicked. The image below shows our current progress.
Although we’ve started adding JSX, no visible UI is shown yet because are yet to return the code from the function Index
.
<div>
<Form
onSubmit={handleSubmit}
render={(formRenderProps) => (
<FormElement
style={{
width: 500,
margin: "200px auto",
}}
horizontal={true}
>
<p style={{ margin: "0px auto" }}>{questions[counter].question}</p>
<fieldset className={"k-form-fieldset"}>
<Field
id={"Answer"}
name={"Answer"}
label={"Answer"}
component={FormInput}
/>
<div className="k-form-buttons">
<Button
themeColor={"primary"}
type={"submit"}
disabled={!formRenderProps.allowSubmit}
>
{counter === questions.length - 1 ? "Submit" : "Next"}
</Button>
</div>
</fieldset>
</FormElement>
)}
/>
</div>
The code above is returned from the function Index
to help React render a visible UI for the site.
The props onSubmit
take a function called handleSubmit
, which is called each time the button component is clicked. The FormElement
is used in controlling the form layout
. The fieldset
prop label defines the string to be rendered, which is received in the FromInput
mentioned previously.
Finally, the prop type
in the component Button
notifies the form to submit whenever the button is clicked. Here’s an image that shows our current progress.
To change the footer and header, go to the root.jsx
file and edit the code in the Layout
function section to your desired header and footer.
function Layout({ children }) {
return (
<div className="remix-app">
<header className="remix-app__header">
<div className="container remix-app__header-content">
<Link to="/" title="Remix" className="remix-app__header-home-link">
my survey website
</Link>
</div>
</header>
<div className="remix-app__main">
<div className="container remix-app__main-content">{children}</div>
</div>
<footer className="remix-app__footer">
<div className="container remix-app__footer-content">
<p></p>
</div>
</footer>
</div>
);
}
Running the site now, you should see the demo project up and running, similar to the image below.
The Remix web framework shows promising tech. The integration with KendoReact could increase the speed in an application development cycle and offer both amazing UI (KendoReact) and a better UX (Remix).
Chinedu is a tech enthusiast focused on full-stack JavaScript and Infrastructure engineering.