Telerik blogs
KendoReactT2_1200x303

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.

Getting Started With Remix

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.

Loading Data Into Your Remix App

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 a test.jsx file in the directory /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.

Linking a Data Source to Your Remix App

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-register 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.

Connecting to the Database

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.

Dynamic Route Params

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.

Building a Survey Site With Remix and React

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 fieldRenderProps 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.

React survey website with Remix - just has header 'My survey website'

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.

remix React app in progress shows question ‘what skills do you have’ with a form field to answer

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.

React-survey-website-with-Remix-complete has header 'My survey website' and question ‘what skills do you have’ with a form field to answer

Conclusion

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
About the Author

Chinedu Imoh

Chinedu is a tech enthusiast focused on full-stack JavaScript and Infrastructure engineering.

Related Posts

Comments

Comments are disabled in preview mode.