Dejan Lukić

How to Create a CRUD App with Deno and React

Originally written for Deno.

Concepts

Overview

React is a front-end framework for building user interfaces based on components. In this guide, you will connect a Deno app with React.

Create React App

Initiate the project by creating a new React app. Open a new terminal window, then run the following command

npx create-react-app deno-react --template typescript

This command will initialize a new project deno-react with support for TypeScript.

Upon installation, you will be presented with the project structure. Navigate to the project by running:

cd deno-react

Create the Router

You will use react-router-dom to create a very crude navigation for the app. Start by importing the necessary dependencies (some will be created later on in the tutorial).

src/index.tsx

import * as React from 'react';
import * as ReactDOM from 'react-dom/client';
import App, { Navigation } from './App';
import './index.css';
import {
	createBrowserRouter,
	RouterProvider,
} from 'react-router-dom';
import Articles from './pages/Articles';
import Dashboard from './pages/Dashboard';

Next, create a router with createBrowserRouter() which will create a router, responsible for navigation and rendering of different components based on the current URL path.

Each route object represents a specific URL path and the corresponding component to render when that path is accessed.

const router = createBrowserRouter([
	{
		path: '/',
		element: <App />,
	},
	{
		path: '/articles',
		element: <Articles />,
	},
	{
		path: '/dashboard',
		element: <Dashboard />,
	},
]);

Continue by rendering the root element in React, with the given RouterProvider object, serving as the router wrapper.

const rootElement = document.getElementById('root');
if (rootElement) {
	ReactDOM.createRoot(rootElement).render(
		<React.StrictMode>
			<RouterProvider router={router} />
		</React.StrictMode>
	);
} else {
	throw new Error('root element not found');
}

Application Login

Now, you will create a basic interface for logging the user in. Start by importing the dependencies (some will be created later on in the tutorial).

src/App.tsx

import React, { useEffect } from 'react'
import {
	Routes,
	Route,
	Link,
	BrowserRouter,
} from 'react-router-dom';
import ProtectedRoute from './pages/ProtectedRoute';
import Dashboard from './pages/Dashboard';
import { isLoggedIn } from './utils/user';
import Articles from './pages/Articles';
import Home from './pages/Home';

type User = {
	id: string;
	name: string;
};

Create a new functional component App that takes props: any as its argument.

export const App = (props: any) => {

Next, create loggedIn which will call the isLoggedIn() method, that checks whether the user is logged in or not.

const loggedIn = isLoggedIn();

Create a new folder src/utils and inside of it a new file user.ts. Continue by creating a new method isLoggedIn():

src/utils/user.ts

export const isLoggedIn = () => {
	const jwt = sessionStorage.getItem('jwt');
	if (!jwt) setUser();
	return !!jwt;
};

The method will try to retrieve a jwt object from the local session storage (in the browser). If the jwt object is not present, the function will try to assign it by calling setUser().

Continue to create a method setUser() which checks for code in the URL parameters (e.g. example.com/abc?code=123).

export const setUser = () => {
	const urlParams = new URLSearchParams(window.location.search);
	const jwt = urlParams.get('code');
	if (jwt) sessionStorage.setItem('jwt', jwt);
	return;
};

Next, create a simple logic that will, based on the truthiness of the loggedIn response, display login anchor or the navigation element with the respective elements.

src/App.tsx

return (
		<>
			{!loggedIn ? (
				<a href='http://localhost:8000/login'>Continue with GitHub</a>
			) : (
				<>
        <Navigation />
        <Routes>
					<Route
						path='/'
						element={
							<ProtectedRoute isLoggedIn={loggedIn}>
								<Home />
							</ProtectedRoute>
						}
					/>
					<Route
						path='/articles'
						element={
							<ProtectedRoute isLoggedIn={loggedIn}>
								<Articles />
							</ProtectedRoute>
						}
					/>
				</Routes>
        </>
			)}
		</>
	);
}

Create the Navigation functional component that will display 2 simple links.

export const Navigation = () => {
	return (
		<nav>
			<Link to='/dashboard'>Dashboard</Link> <br />
			<Link to='/articles'>Articles</Link>
		</nav>
	);
};

export default App;

Articles Page

Create a new folder pages that will contain all the pages. Inside of that folder, create a new file Articles.tsx. Import the necessary dependencies:

src/pages/Articles.tsx

import { render } from 'react-dom';
import React, { useEffect, useState } from 'react';

Initialize a new functional component and a new useState() hook that will handle the articles.

export const Articles = () => {
	const [articles, setArticles] = useState([]);

Upon the page load, the page will display all articles present in the Deno KV. To do so, you will use the useEffect hook. The hook will perform a GET request to the Deno endpoint, fetching the articles and storing them with the previous useState hook.

useEffect(() => {
		fetch('http://localhost:8000/read')
			.then((response) => response.json())
			.then((data) => setArticles(data.data));
	}, []);

Next, define a deleteArticle() method that will handle the article deletion.

const deleteArticle = (name: string) => {
		fetch(`http://localhost:8000/delete/${name}`, {
			method: 'DELETE',
		});
		const newArticles = articles.filter((article: any) => article.name !== name);
		setArticles(newArticles);
	};

After completing the DELETE request to the Deno endpoint, the React will perform an optimistic update with the article removed.

To update the article, you will create a similar method updateArticle():

const updateArticle = async (name: string) => {
		const name = prompt('Update name');
		const formData = {
			name,
		};
		const res = await fetch(`http://localhost:8000/update/${name}`, {
			method: 'PUT',
			headers: {
				'Content-Type': 'application/json',
			},
			body: JSON.stringify(formData),
		});
		window.location.reload();
	};

After the PUT request to the endpoint, another optimistic update will happen by reloading the window. This usually shouldn’t be done in the production, but for the sake of the simplicity you can do this here.

To render the articles, create a simple renderArticles() method:

const renderArticles = (articles: any) => {
		return (
			<>
				<h1>Articles</h1>
				<ul>
					{articles.map((article: any) => (
						<li key={article.name}>
							<h2>{article.name}</h2>
							<p>{article.content}</p>
							<button onClick={() => deleteArticle(article.name)}>Delete article</button> <br />
							<button onClick={() => updateArticle(article.name)}>Update article</button>
						</li>
					))}
				</ul>
			</>
		);
	};

The method will map all the articles from the articles object in an unordered list.

Finally, return a JSX element if the there are some articles available:

return (
		<>
		{articles.length > 0 ? renderArticles(articles) : <p>Loading...</p>}
		</>
	);
}
export default Articles;

Dashboard Page

To create new articles, the app will have a page to enter the article name and its content.

Create a new file Dashboard.tsx in the src/pages folder. Import the necessary dependencies:

src/pages/Dashboard.tsx

import { retrieveUser } from '../utils/user';
import React, { useState } from "react";

Create a new functional component and a useState hook for the success of the operation.

const Dashboard = () => {
	const [success, setSuccess] = useState(false);

To handle button events, you will create a custom event handler method handleSubmit():

const handleSubmit = async (e: any) => {
		e.preventDefault();
		const { name, content } = e.target.elements;
		const formData = {
			name: name.value,
			content: content.value,
			author: retrieveUser().user,
		};
		const res = await fetch('http://localhost:8000/create', {
			method: 'POST',
			headers: {
				'Content-Type': 'application/json',
			},
			body: JSON.stringify(formData),
		});
		const data = await res.json();
		if (data.success) setSuccess(true);
	};

Upon pressing the submit button, the default event will be prevented and the handleSubmit() method will be executed. The method sends a POST request to the Deno endpoint, creating the article and updating the success hook.

return (
		<>
			<h1>Dashboard</h1>
			<h2>Create article</h2>
			<form onSubmit={handleSubmit} className='inputs'>
				<input type='text' name='name' placeholder='Article name' />
				<textarea
					name='content'
					id='content'
					placeholder='Content'
					cols={30}
					rows={10}></textarea>
				<button type='submit'>Submit</button>
				{success && <p>Article created successfully!</p>}
			</form>
		</>
	);
export default Dashboard;

Create a simple home page in the src/pages/Home.tsx file.

src/pages/Home.tsx

import * as React from 'react';
import { Navigation } from "../App";

const Home = () => {
    return (
        <>                
        <h1>Home</h1>
        </>
    );
};
export default Home;

Protected Route Handler

To prevent users from seeing unauthorized content, you will need to create a protected route.

src/pages/ProtectedRoute.tsx

import * as React from "react";
import { Navigate } from "react-router-dom";
const ProtectedRoute = ({ isLoggedIn, children }: any) => {
  if (!isLoggedIn) {
    return <Navigate to="/" replace />;
  }
  return children;
};
export default ProtectedRoute;

The functional component ProtectedRoute takes an object as its parameter, which includes the isLoggedIn boolean property and children components property.

If the user isn’t logged in, the user will be redirected to the login screen, otherwise the content will be shown.

Testing

To test the app, run the following command in the terminal:

npm run start

This will start the server and bundle the React. You can access the webpage by navigating to http://localhost:3000. To authenticate press Continue with GitHub which will redirect to the GitHub’s OAuth2 provider app.

Test the CRUD operations in the Dashboard section.

#api #crud #deno #react