Frontend - React
Prerequisites
- Deployed Todos API
- NodeJS
Setup
- In your terminal spin-up a new React Project
npx create-react-app@latest todofront
- CD into new folder and Install support Libraries to be used
npm install react-router-dom milligram
- test out dev server
npm start
and go to localhost:3000
Setting Up React Router
Let's start out by setting up our router files in the src:
- router.js
- actions.js
- loaders.js
In router.js let's add the following:
import {createBrowserRouter, createRoutesFromElements, Route, Routes} from "react-router-dom"
import App from "./App"
const router = createBrowserRouter(createRoutesFromElements(
<>
<Route path="/" element={<App/>}>
</Route>
</>
))
export default router
Let's setup Router and Milligram in our index.js.
import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import reportWebVitals from './reportWebVitals';
import "milligram"
import {RouterProvider} from "react-router-dom"
import router from './router';
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<React.StrictMode>
<RouterProvider router={router}/>
</React.StrictMode>
);
// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();
Setting Up Our Files
- In src create a
components
folder for holding small pieces of UI and apages
folder for components that act routes/pages.
Create the following Components
src/components/Post.js
const Post = (props) => {
return <h1>Post</h1>;
};
export default Post;
src/pages/Index.js
const Index = (props) => {
return <h1>Index</h1>;
};
export default Index;
src/pages/Show.js
const Show = (props) => {
return <h1>Show</h1>;
};
export default Show;
Setting up our routes
So now it's time to bring our components into our App.js where we will setup four client-side routes.
- "/" -> Our index page showing all posts
- "/post/:id" -> show page showing a single post
- "/create" -> for our new form to submit to
- "/update/:id" -> for edit form to submit to
- "/delete/:id" -> for our delete forms to submit to
/src/router.js
import {createBrowserRouter, createRoutesFromElements, Route} from "react-router-dom"
import App from "./App"
import Index from "./pages/Index"
import Show from "./pages/Show"
const router = createBrowserRouter(createRoutesFromElements(
<>
<Route path="/" element={<App/>}>
<Route path="" element={<Index/>}/>
<Route path="post/:id" element={<Show/>}/>
<Route path="create"/>
<Route path="update/:id"/>
<Route path="delete/:id"/>
</Route>
</>
))
export default router
/src/App.js
import {Outlet} from "react-router-dom"
function App() {
return (
<div className="App">
<Outlet/>
</div>
);
}
export default App;
Getting Our Todos
We will create two loaders
- indexLoader: Gets all the todos for the Index page
- showLoader: Gets a single todo for the Show page
We can then use the data on those pages with useLoaderData hook. Let's write the loaders in loaders.js.
loaders.js
// YOUR DEPLOYED API BASE URL
const URL = "https://xxxxxxx.onrender.com"
//indexLoader => get all todos for index route
export const indexLoader = async () => {
const response = await fetch(URL + "/todos/")
const todos = await response.json()
return todos
}
//showLoader => get a single todo for Show route
export const showLoader = async ({params}) => {
const response = await fetch(URL + `/todos/${params.id}/`)
const todo = await response.json()
return todo
}
let's attach our loaders to their routes
router.js
import {createBrowserRouter, createRoutesFromElements, Route} from "react-router-dom"
import App from "./App"
import { indexLoader, showLoader } from "./loaders"
import Index from "./pages/Index"
import Show from "./pages/Show"
const router = createBrowserRouter(createRoutesFromElements(
<>
<Route path="/" element={<App/>}>
<Route path="" element={<Index/>} loader={indexLoader}/>
<Route path="post/:id" element={<Show/>} loader={showLoader}/>
<Route path="create"/>
<Route path="update/:id"/>
<Route path="delete/:id"/>
</Route>
</>
))
export default router
Rendering Our Todos
The Index route should now be loading all the todos, let's pull them in and render them using the useLoaderData hook from react-router-dom.
src/pages/Index.js
import Post from "../components/Post";
import {useLoaderData} from "react-router-dom"
const Index = (props) => {
const todos = useLoaderData()
// For each post in the array render a Post component
return todos.map((post) => <Post post={post} key={post.id} />);
};
export default Index;;
Let's define how an individual post will look like in src/components/post.js
import { Link } from "react-router-dom";
//destructure the post from props
const Post = ({ post }) => {
//////////////////
// Style Objects
//////////////////
const div = {
textAlign: "center",
border: "3px solid",
margin: "10px auto",
width: "80%",
};
return (
<div style={div}>
<Link to={`/post/${post.id}`}>
<h1>{post.subject}</h1>
</Link>
<h2>{post.details}</h2>
</div>
);
};
export default Post
SinglePost Component!
Our component to see an individual post, src/pages/SinglePost.js
import { Link, useLoaderData } from "react-router-dom";
// destructuring the props needed to get our post, including router prop match
const Show = () => {
const post = useLoaderData();
////////////////////
// Styles
///////////////////
const div = {
textAlign: "center",
border: "3px solid green",
width: "80%",
margin: "30px auto",
};
return (
<div style={div}>
<h1>{post.subject}</h1>
<h2>{post.details}</h2>
<Link to="/">
<button>Go Back</button>
</Link>
</div>
);
};
export default Show;
Cool, we can now see our todos!
Setting Up Our Forms
Let's start by defining the actions that each of our forms will submit to:
- createAction: will take the data from our form and make a call to create route of our API
- updateAction: will take the data from our form and make a call to the update route of our API
- deleteAction: will make a call to the delete route of our api
actions.js
import { redirect } from "react-router-dom"
// YOUR DEPLOYED API BASE URL
const URL = "https://xxxxxxx.onrender.com"
//createAction => create a todo from form submissions to `/create`
export const createAction = async ({request}) => {
// get form data
const formData = await request.formData()
// construct request body
const newTodo = {
subject: formData.get("subject"),
details: formData.get("details")
}
// send request to backend
await fetch(URL + "/todos/", {
method: "post",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify(newTodo)
})
// redirect back to index page
return redirect("/")
}
//updateAction => update a todo from form submissions to `/update/:id`
export const updateAction = async ({request, params}) => {
// get form data
const formData = await request.formData()
// get todo id
const id = params.id
// construct request body
const updatedTodo = {
subject: formData.get("subject"),
details: formData.get("details")
}
// send request to backend
await fetch(URL + `/todos/${id}/`, {
method: "put",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify(updatedTodo)
})
// redirect back to show page page
return redirect(`/post/${id}`)
}
//deleteAction => delete a todo from form submissions to `/delete/:id`
export const deleteAction = async ({params}) => {
// get todo id
const id = params.id
// send request to backend
await fetch(URL + `/todos/${id}/`, {
method: "delete",
})
// redirect back to show page page
return redirect("/")
}
Let's connect those actions to our routes in router.js
import {
createBrowserRouter,
createRoutesFromElements,
Route
} from "react-router-dom";
import App from "./App";
import { indexLoader, showLoader } from "./loaders";
import Index from "./pages/Index";
import Show from "./pages/Show";
import { createAction, updateAction, deleteAction } from "./actions";
const router = createBrowserRouter(
createRoutesFromElements(
<>
<Route path="/" element={<App />}>
<Route path="" element={<Index />} loader={indexLoader} />
<Route path="post/:id" element={<Show />} loader={showLoader} />
<Route path="create" action={createAction}/>
<Route path="update/:id" action={updateAction}/>
<Route path="delete/:id" action={deleteAction}/>
</Route>
</>
)
);
export default router;
Creating a Todo
Let's add a form to our index page to create a todo.
pages/Index.js
import Post from "../components/Post";
import {useLoaderData} from "react-router-dom"
import { Form } from "react-router-dom";
const Index = (props) => {
const todos = useLoaderData()
// For each post in the array render a Post component
return <>
<div style={{textAlign: "center"}}>
<h2>Create a Todo</h2>
<Form action="/create" method="post">
<input type="text" name="subject" placeholder="write subject here"/>
<input type="text" name="details" placeholder="write details here"/>
<button>Create New Todo</button>
</Form>
</div>
{todos.map((post) => <Post post={post} key={post.id} />)}
</>;
};
export default Index;
Editing and Deleting Todos
Now we just need to add a Form for updated and a Form for deleting in our Show page and we're good to go!
pages/Show.js
import { Link, useLoaderData, Form } from "react-router-dom";
// destructuring the props needed to get our post, including router prop match
const Show = () => {
const post = useLoaderData();
////////////////////
// Styles
///////////////////
const div = {
textAlign: "center",
border: "3px solid green",
width: "80%",
margin: "30px auto",
};
return (
<div style={div}>
<h1>{post.subject}</h1>
<h2>{post.details}</h2>
<div style={{ textAlign: "center" }}>
<h2>Create a Todo</h2>
<Form action={`/update/${post.id}`} method="post">
<input
type="text"
name="subject"
placeholder="write subject here"
defaultValue={post.subject}
/>
<input
type="text"
name="details"
placeholder="write details here"
defaultValue={post.details}
/>
<button>Update Todo</button>
</Form>
<Form action={`/delete/${post.id}`} method="post">
<button>Delete Todo</button>
</Form>
</div>
<Link to="/">
<button>Go Back</button>
</Link>
</div>
);
};
export default Show;
You have achieved Full CRUD!
Now you can deploy the static site to Vercel, Render or Netlify!