How to Create a CRUD App with Deno and React
Originally written for Deno.
Concepts
- Use Deno with React to create an interactive CRUD app.
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.