This month has been crazy learning about a TON of new and different “user security” topics. It was very overwhelming at times. Here are all of the different aspects of the React side for users which I learned and developed:
- Fetching Secure Data from Django
- Enabling User Login
- Automatic Token Refresh
- User Logout
- Registration
- Protected Routes
- User Menu Changes
- Converting the Class Components to Functional Components
- Environment Settings
Fetching Secure Data from Django
Fetching Data from Django was no easy task. Originally, all I needed was a basic Axios get (or put or delete). But when you conditionally incorporate tokens into the headers, things can get complex.
Conditionally: only when the user is authenticated (and how to track that) should the token be placed into the header. In my environment, upon login, the code was able to properly dynamically add the token to the header. Upon the next component load, despite the token fully loaded in a cookie and local storage, the code would always skip the entire Axios request and response interceptor. So, I had to bail on using Axios (it’s more important to have the application working than a certain library, right?!)
The basic JavaScript fetch is what I was able to get to correctly conditionally add the authorization clause to the headers.
Enabling User Login
At CalPERS, due to our tiny custom framework, we didn’t tend to use publicly available modules to do our work: everything was custom-built. At first, I went down the path of “No problem: I can write all of the user authentication states and tokens myself”. Two days later, I gave up and started searching for JavaScript modules that could take care of User Authentication state management instead of me. I used the react-auth-kit to manage the user state. I hoped that this kit would become a simple catch-all for everything user authentication, but there were more items that I would have to write myself.
I decided to have both access and refresh tokens to be saved in local storage so that they could be used across multiple browser tabs and windows. Oftentimes, a user (like me) will go back and forth between searching existing postings and adding new ones while sometimes also having a weekly status report open.
I also chose to store the refresh token in the HTTP cookie managed by react-auth-kit. Since I don’t have certificates set up locally, I set cookieSecure to false. For a typical production app, I would know better and always have HTTPS enabled and only use secured cookies.
My other key workaround from the above issue: traffic to the back end only uses Axios to access the login endpoint (everywhere secure uses fetch).
// Login.jsx
import React, { useState } from "react";
import { useNavigate } from "react-router-dom";
import { useSignIn } from "react-auth-kit"; // Correct hook to handle login
import axios from "axios";
import { Container, Form, Card, CardTitle, CardBody, CardFooter, FormGroup, Label, Input } from "reactstrap";
const Login = () => {
const [username, setUserName] = useState("");
const [password, setPassword] = useState("");
const [error, setError] = useState(""); // Error handling state
const navigate = useNavigate(); // Hook to navigate after login
const signIn = useSignIn(); // Use useSignIn hook to handle login
const handleLogin = async (e) => {
e.preventDefault();
try {
// Make the POST request to your Django login API (replace URL with your Django endpoint)
const apiUrl = process.env.REACT_APP_API_URL;
if (!apiUrl) {
throw new Error("API URL is not defined in environment variables");
}
const response = await axios.post(`${apiUrl}auth/login/`, { username, password });
// Assuming the response contains the token and user info
const { access, refresh, user } = response.data;
// Store the access token in LocalStorage
localStorage.setItem("access_token", access);
localStorage.setItem("refresh_token", refresh);
// Call signIn from react-auth-kit
const isSignedIn = signIn({
token: refresh,
expiresIn: 3600, // Token expiration time in seconds
tokenType: "Bearer",
authState: user, // Save user info
sameSite: 'None', // This allows the cookie to be sent in cross-origin requests
});
if (!isSignedIn) {
throw new Error("Failed to sign in. Please try again.");
}
navigate("/dashboard"); // Redirect to the protected page after successful login
} catch (error) {
// Check if the error response contains a message to display
if (error.response && error.response.data.error && error.response.data.detail) {
setError(error.response.data.detail); // Display backend error message
} else {
setError(error.message || "An error occurred. Please try again.");
}
}
};
return (
<Container className="centered-container">
<Form onSubmit={handleLogin}>
<Card className="text-dark bg-light m-3 card-narrow">
<CardTitle><strong>Login</strong></CardTitle>
<CardBody>
<FormGroup>
<Label>User Name</Label>
<Input
type="text"
value={username}
onChange={(e) => setUserName(e.target.value)}
/>
</FormGroup>
<FormGroup>
<Label>Password</Label>
<Input
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
/>
</FormGroup>
{error && <p style={{ color: "red" }}>{error}</p>} {/* Show error message */}
</CardBody>
<CardFooter>
<button type="submit">Login</button>
</CardFooter>
</Card>
</Form>
</Container>
);
};
export default Login;
The react-auth-kit was used in four places: Login, Logout, checking protected routes, and the index.js AuthProvider. Here is how I implemented the AuthProvider code:
// index.js
import React, { StrictMode } from 'react';
import ReactDOM from 'react-dom/client';
import { BrowserRouter } from 'react-router-dom';
import { AuthProvider } from 'react-auth-kit';
import App from './App';
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<AuthProvider authType="cookie" authName="_auth"
cookieDomain={window.location.hostname} cookieSecure={false}>
<BrowserRouter>
<StrictMode>
<App />
</StrictMode>
</BrowserRouter>
</AuthProvider>
);
Automatic Token Refresh
Conceptually, “get a token, store the token, refresh the token” is rather simple. But to implement it in react, coupled with the react component double render (send traffic to the back end twice), quickly became problematic. One of the benefits of JavaScript and React is its ability to do multi-threading. Unfortunately, you can’t multi-thread traffic to the back end when you are refreshing the token. You’ll kick off the first call to the back end then await it to come back. Meanwhile, another post is kicked off to the back end. While the first refreshed token is coming back, a second one is requested and… that loop would be enough to drive most…
The variable isRefreshing
is used to prevent duplicate refresh attempts. Without isRefreshing
, multiple 401 responses from concurrent requests would trigger multiple calls to refreshAccessToken
. This caused issues with the refresh endpoint. The isRefreshing
flag acts as a single-entry guard to ensure only one token refresh process is started.
The code uses the refreshPromise
to ensure that all requests share the same promise. If one request triggers a token refresh, other concurrent requests that encounter 401 responses wait on refreshPromise
instead of starting new refresh attempts. This avoids redundant network calls and ensures that all requests get the same updated token. The promise tracks the ongoing refresh operation and allows all dependent requests to respond appropriately based on the result.
The combination in action:
- First 401 response:
isRefreshing
isfalse
, so the code setsisRefreshing = true
and startsrefreshPromise = refreshAccessToken()
.
- Subsequent 401 responses during the same refresh:
isRefreshing
istrue
, so they wait onrefreshPromise
.
- Once the refresh completes:
- The
refreshPromise
resolves or rejects. - All waiting requests proceed with the new token or handle the failure.
isRefreshing
andrefreshPromise
are reset in thefinally
block.
- The
Why this design?
The design ensures efficiency, prevents redundant refresh attempts, and allows smooth handling of concurrent requests needing a refreshed token. Both variables serve complementary purposes:
isRefreshing
acts as a quick flag to prevent initiating multiple refreshes.refreshPromise
is the actual mechanism to share the result of the ongoing refresh among all waiting requests.
//customFetch.js
import { TOKEN_REFRESH_API_URL } from "./constants";
export async function customFetch(url, options = {}, accessToken, refreshToken, navigate) {
let isRefreshing = false; // Track if the token refresh process is ongoing
let refreshPromise = null; // To store the promise of the ongoing refresh process
try {
// Helper function to refresh the token
async function refreshAccessToken() {
const response = await fetch(TOKEN_REFRESH_API_URL, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ refresh: refreshToken }),
});
if (!response.ok) {
// If refreshing fails, navigate to login
navigate('/login');
throw new Error('Failed to refresh access token');
}
const data = await response.json();
return data.access; // The new access token
}
// Set up headers with the access token
const headers = {
'Content-Type': 'application/json',
...(accessToken && { Authorization: `Bearer ${accessToken}` }),
};
// Ensure the body is correctly included for POST/PUT methods
const body =
(options.method === 'POST' || options.method === 'PUT' || options.method === 'DELETE')
? JSON.stringify(options.body)
: null;
let response = await fetch(url, {
...options,
headers,
body, // Add the body to the request
});
if (response.status === 401 && refreshToken && !isRefreshing) {
// Access token expired, start the refresh process
isRefreshing = true;
console.log('Access token expired, attempting to refresh...');
// Start refreshing the access token only once
refreshPromise = refreshAccessToken();
// Wait for the refresh to complete and get the new token
const newAccessToken = await refreshPromise;
// Save the new access token to localStorage
localStorage.setItem('access_token', newAccessToken);
// Now retry the original request with the new access token
const retryHeaders = {
'Content-Type': 'application/json',
Authorization: `Bearer ${newAccessToken}`,
};
response = await fetch(url, {
...options,
headers: retryHeaders,
body, // Retry the body if needed
});
if (!response.ok) {
throw new Error(`HTTP Error: ${response.status} - ${response.statusText}`);
}
// Parse and return the JSON data from the retried response
const retryData = await response.json();
return retryData || [];
}
// If no refresh needed, handle the normal response
if (!response.ok && response.status !== 401) {
console.error(`HTTP Error: ${response.status} - ${response.statusText}`);
}
// Handle empty or non-JSON response body
const contentType = response.headers.get('Content-Type');
if (!contentType || !contentType.includes('application/json')) {
const text = await response.text();
return text.trim().length === 0 ? [] : [];
}
// Parse and return JSON data
const data = await response.json();
return data || [];
} catch (error) {
console.error('Custom fetch error:', error);
return [];
} finally {
// Reset the refresh flag once the refresh process is done
isRefreshing = false;
refreshPromise = null;
}
}
Wrapping customFetch with useApiRequest creates a more structured and reusable API interaction mechanism tailored for use within react components. It enables:
- Centralized Error Handling – The
useApiRequest
hook captures errors (setError
) in a state variable that can be easily accessed by React components. - React State Integration –
useState
: Manages theerror
state, which is tied to the React component lifecycle. This enables re-renders and seamless error updates in the UI. - Memoization for Stable References –
useCallback
: Ensures thatapiRequest
has a stable reference, avoiding unnecessary re-creations and improving performance, especially when passed as a dependency to other hooks (e.g.,useEffect
) or components. React re-renders only when one of the dependencies (navigate
,accessToken
,refreshToken
) changes. - Simplified and Reusable API Calls – The hook abstracts away repetitive setup
// useApiRequest.js
import { useNavigate } from 'react-router-dom'; // To redirect to login on failure
import { useState, useCallback } from 'react'; // Import useCallback
import { customFetch } from './customFetch'; // import customFetch
export function useApiRequest() {
const navigate = useNavigate();
const [error, setError] = useState(null);
const accessToken = localStorage.getItem("access_token");
const refreshToken = localStorage.getItem("refresh_token");
// Use useCallback to memoize apiRequest
const apiRequest = useCallback(async (url, body = {}, options = {}) => {
try {
const response = await customFetch(url, { ...options, body }, accessToken, refreshToken, navigate);
return response; // Return the response for use in components
} catch (err) {
setError(err.message); // Set error in case of failure
console.error(err);
return null;
}
}, [navigate, accessToken, refreshToken]); // Add these as dependencies to ensure stable memoization
return { apiRequest, error };
}
User Logout
The logout does all of the cleanup:
- Removes the localStorage entries
- Calls the back end, which invalidates the tokens
- Calls the
react-auth-kit
signOut
which, among other things, removes the saved cookie
Unlike the login which only needs to be accessible from its own page, logout needs to be available everywhere. The function was placed in the app.js component itself.
const handleLogout = async () => {
// Make the logout API call to invalidate the token
await apiRequest('/api/logout/', {}, { method: 'POST' }); // Adjust the endpoint if needed
signOut(); // Sign out the user
localStorage.removeItem("access_token");
navigate("/");// Redirect to home page after logout
};
Registration
The Register component has regular expression validations for the username, password, and email. The error reporting has two parts:
- Front-end error handling, showing up right next to the field itself
- Back-end error handling, which could be one or many rows, even just associated with one field. Django is the first framework that I have worked with where I was able to use advanced validators. It is the Django output that is displayed at the bottom of the component, right above the footer itself.
If I had extra time, I would have integrated the react-hook-form in these new functional components, which I wasn’t using before (they used to be class components). You’ll also notice that the code sources the react env file to determine whether to enforce strong passwords or not. And, one of my favorite features on other sites: show and hide the password. This way, the user can choose to see the password that they entered.
import React, { useState } from "react";
import { useNavigate } from "react-router-dom";
import axios from "axios";
import { Container, Form, Card, CardTitle, CardBody, CardFooter, Row, Col, FormGroup, Label, Input } from "reactstrap";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faEye, faEyeSlash } from "@fortawesome/free-solid-svg-icons";
const ENFORCE_STRONG_PASSWORD = process.env.REACT_APP_ENFORCE_STRONG_PASSWORD === "true";
export default function Register() {
const [username, setUserName] = useState("");
const [password, setPassword] = useState("");
const [firstName, setFirstName] = useState("");
const [lastName, setLastName] = useState("");
const [email, setEmail] = useState("");
const [showPassword, setShowPassword] = useState(false);
const [passwordError, setPasswordError] = useState("");
const [usernameError, setUsernameError] = useState("");
const [emailError, setEmailError] = useState("");
const [backendErrors, setBackendErrors] = useState([]);
const navigate = useNavigate();
const handlePasswordToggle = () => {
setShowPassword((prevState) => !prevState);
};
const validateUserName = (username) => {
const usernameRegex = /^(?=.{3,20}$)(?![_\-.])(?!.*[_\-.]{2})[a-zA-Z0-9._-]+(?<![_\-.])$/;
if (!usernameRegex.test(username)) {
setUsernameError(
"Username must be 3–20 characters long, alphanumeric, and can include underscores (_), dots (.), or hyphens (-). It cannot start, end, or have consecutive special characters."
);
return false;
}
setUsernameError(""); // Clear error if validation passes
return true;
}
const validatePassword = (password) => {
if (!ENFORCE_STRONG_PASSWORD) {
return true; // No validation if the feature is disabled
}
const strongPasswordRegex =
/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[!@#$%^&*(),.?":{}|<>])[A-Za-z\d!@#$%^&*(),.?":{}|<>]{12,}$/;
if (!strongPasswordRegex.test(password)) {
setPasswordError(
"Password must be at least 12 characters long, include at least one uppercase letter, one lowercase letter, one number, and one special character."
);
return false;
}
setPasswordError(""); // Clear error if validation passes
return true;
};
const validateEmail = (email) => {
const sanitizedEmail = email.trim(); // Trim whitespace
const emailRegex = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,20}$/;
if (!emailRegex.test(sanitizedEmail)) {
setEmailError(
"Please enter a valid email address."
);
console.log("Email Address Tested: " + email)
return false;
}
setEmailError(""); // Clear error if validation passes
return true;
}
const handleSubmit = async (e) => {
e.preventDefault();
// Run all validations before proceeding
const isUsernameValid = validateUserName(username);
const isPasswordValid = validatePassword(password);
const isEmailValid = validateEmail(email);
// If any validation fails, stop form submission
if (!isUsernameValid || !isPasswordValid || !isEmailValid) {
console.error("Validation failed. Please fix the errors and try again.");
return;
}
try {
const apiUrl = process.env.REACT_APP_API_URL;
const response = await axios.post(`${apiUrl}auth/register/`, {
username,
password,
first_name: firstName,
last_name: lastName,
email,
});
if (response.status === 201) { // Assuming 201 Created for successful registration
console.log("Registration successful:", response.data);
navigate("/login"); // Redirect to login page after successful registration
} else {
console.error("Unexpected response during registration:", response);
}
} catch (err) {
console.error("Error during registration:", err);
console.error(err.response?.data?.error)
setBackendErrors(
Array.isArray(err.response?.data?.error)
? err.response.data.error
: typeof err.response?.data?.error === 'string'
? err.response?.data?.error.startsWith('[') // Check if the string looks like an array
? JSON.parse(err.response?.data?.error.replace(/'/g, '"')) // Parse the string as JSON if it's an array-like string
: [err.response?.data?.error] // Otherwise, wrap the single error in an array
: ['An unknown error occurred']
);
}
}
return (
<Container className="centered-container">
<Form onSubmit={handleSubmit}>
<Card className="text-dark bg-light m-3 card-medium">
<CardTitle><strong>Register</strong></CardTitle>
<CardBody className="bg-white">
<Row id="full_name" >
<Col id="first_name" >
<FormGroup>
<Label>First Name</Label>
<Input
type="text" required
value={firstName}
onChange={(e) => setFirstName(e.target.value)}
/>
</FormGroup>
</Col>
<Col id="last_name" >
<FormGroup>
<Label>Last Name</Label>
<Input
type="text" required
value={lastName}
onChange={(e) => setLastName(e.target.value)}
/>
</FormGroup>
</Col>
</Row>
<Row id="user_pass" >
<Col id="user_name" >
<FormGroup>
<Label>User Name</Label>
<Input
type="text" required
value={username}
onChange={(e) => setUserName(e.target.value)}
onBlur={() => validateUserName(username)}
/>
{usernameError && (
<div className="text-danger mt-1">{usernameError}</div>
)}
</FormGroup>
</Col>
<Col id="pass" >
<FormGroup>
<Label>Password</Label>
<div className="input-group">
<Input
type={showPassword ? "text" : "password"}
value={password}
onChange={(e) => setPassword(e.target.value)}
onBlur={() => validatePassword(password)}
/>
<span
className="input-group-text"
onClick={handlePasswordToggle}
style={{ cursor: "pointer" }}
>
<FontAwesomeIcon
icon={showPassword ? faEyeSlash : faEye}
/>
</span>
</div>
{passwordError && (
<div className="text-danger mt-1">{passwordError}</div>
)}
</FormGroup>
</Col>
</Row>
<Row>
<Col>
<FormGroup>
<Label>Email Address</Label>
<Input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
onBlur={() => validateEmail(email)}
/>
{emailError && (
<div className="text-danger mt-1">{emailError}</div>
)}
</FormGroup>
</Col>
</Row>
{backendErrors.length > 0 && (
<Row className="error-summary alert alert-danger mb-4">
<Col>
<h5>Please correct the following:</h5>
<ul>
{backendErrors.map((error, index) => (
<li key={index}>{error}</li>
))}
</ul>
</Col>
</Row>
)}
</CardBody>
<CardFooter>
<button type="submit">Register</button>
</CardFooter>
</Card>
</Form>
</Container>
)
}
Protected Routes
When a user tries to navigate to any given react route, the routes that require authentication need to check whether the user is correctly logged in or not. The route protection uses the react-auth-kit
to ensure that the user is authenticated.
import React from 'react';
import { Navigate } from 'react-router-dom';
import { useIsAuthenticated } from 'react-auth-kit';
const ProtectedRoute = ({ element, ...rest }) => {
const isAuthenticated = useIsAuthenticated();
// If the user is not authenticated, redirect to the login page
if (!isAuthenticated()) {
return <Navigate to="/login" />;
}
// If authenticated, render the passed-in element
return element;
};
export default ProtectedRoute;
User Menu Changes
I think that the most fun I had with this particular release was the changes to the navigation bar and router. The navigation bar and router render differently based on authentication using the react-auth-kit isAuthenticated
.
When a user is not authenticated:
- the nav bar only has Links to all of the static documents with no Information sub-menu.
- The far right end of the nav bar has the Login and Register elements
- The root is routed to the Welcome component.
When a user is authenticated:
- All of the documentation content is in the Information sub-menu.
- There is no ‘Welcome’ component: the Dashboard component exists instead
- The far right end of the nav bar has the user name with the sub-menu for logout (and other eventual user functions)
- The root is routed to the Dashboard component.
Here is how my final app.js code became:
<div>
<Navbar color="light" light expand="md">
<NavbarBrand href="/">Job Search Tracker</NavbarBrand>
<NavbarToggler onClick={toggle} />
<Collapse isOpen={isOpen} navbar>
{isAuthenticated() ? (
<Nav className="ml-auto" navbar>
<NavItem>
<NavLink href="/dashboard">Dashboard</NavLink>
</NavItem>
{/* Static Pages Submenu */}
<UncontrolledDropdown nav inNavbar>
<DropdownToggle nav caret>
Information
</DropdownToggle>
<DropdownMenu end>
<DropdownItem tag={Link} to="/about">About</DropdownItem>
<DropdownItem tag={Link} to="/job-hunt-tips">Job Hunt Tips</DropdownItem>
<DropdownItem tag={Link} to="/boolean-search">Boolean Search</DropdownItem>
<DropdownItem tag={Link} to="/financial-assistance">Financial Assistance Programs</DropdownItem>
<DropdownItem divider />
<DropdownItem tag={Link} to="/release-history">Release History</DropdownItem>
</DropdownMenu>
</UncontrolledDropdown>
<NavItem>
<NavLink href="/job-sites">Job Sites</NavLink>
</NavItem>
<NavItem>
<NavLink href="/job-postings">Job Postings</NavLink>
</NavItem>
<NavItem>
<NavLink href="/opportunities">Opportunities</NavLink>
</NavItem>
{/* Reports Submenu */}
<UncontrolledDropdown nav inNavbar>
<DropdownToggle nav caret>
Reports
</DropdownToggle>
<DropdownMenu end>
<DropdownItem tag={Link} to="/reports/postingsAppliedSince">Postings Applied</DropdownItem>
<DropdownItem tag={Link} to="/reports/perSite">Per Site</DropdownItem>
<DropdownItem tag={Link} to="/reports/perWeek">Per Week</DropdownItem>
</DropdownMenu>
</UncontrolledDropdown>
</Nav>
) : (
<Nav className="ml-auto" navbar>
<NavItem><NavLink href="/">Welcome</NavLink></NavItem>
<NavItem><NavLink href="/about">About</NavLink></NavItem>
<NavItem><NavLink href="/job-hunt-tips">Job Hunt Tips</NavLink></NavItem>
<NavItem><NavLink href="/boolean-search">Boolean Search</NavLink></NavItem>
<NavItem><NavLink href="/financial-assistance">Financial Assistance Programs</NavLink></NavItem>
<NavItem><NavLink href="/release-history">Release History</NavLink></NavItem>
</Nav>
)}
<Nav className="ms-auto" navbar>
{isAuthenticated() ? (
<UncontrolledDropdown nav inNavbar>
<DropdownToggle nav caret>
Hi {user()?.first_name || 'First Name Not Available'}!
</DropdownToggle>
<DropdownMenu end>
<DropdownItem tag={Link} to="/edit-profile">Edit Profile</DropdownItem>
<DropdownItem divider />
<DropdownItem onClick={handleLogout}>Logout</DropdownItem>
</DropdownMenu>
</UncontrolledDropdown>
) : (
<>
<NavItem>
<NavLink tag={Link} to="/login">Login</NavLink>
</NavItem>
<NavItem>
<NavLink tag={Link} to="/register">Register</NavLink>
</NavItem>
</>
)}
</Nav>
</Collapse>
</Navbar>
<Routes>
{/* Conditional Routing for / based on Authentication */}
<Route path="/" element={isAuthenticated() ? <Dashboard /> : <Welcome />} />
{/* Public Routes */}
<Route path="/welcome" element={<Welcome />} />
<Route path="/login" element={<Login />} />
<Route path="/register" element={<Register />} />
<Route path="/about" element={<About />} />
<Route path="/job-hunt-tips" element={<JobHuntTips />} />
<Route path="/release-history" element={<ReleaseHistory />} />
<Route path="/financial-assistance" element={<FinancialAssistance />} />
<Route path="/boolean-search" element={<BooleanSearch />} />
{/* Protected Routes */}
<Route path="/secret" element={<ProtectedRoute element={<Secret />} />} />
<Route path="/dashboard" element={<ProtectedRoute element={<Dashboard />} />} />
<Route path="/job-sites" element={<ProtectedRoute element={<JobSiteList />} />} />
<Route path="/job-site-view/:id" element={<ProtectedRoute element={<JobSiteView />} />} />
<Route path="/job-site-new" element={<ProtectedRoute element={<JobSiteEdit />} />} />
<Route path="/job-site-new/:id" element={<ProtectedRoute element={<JobSiteEdit />} />} />
<Route path="/job-site-edit" element={<ProtectedRoute element={<JobSiteEdit />} />} />
<Route path="/job-site-edit/:id" element={<ProtectedRoute element={<JobSiteEdit />} />} />
<Route path="/job-postings" element={<ProtectedRoute element={<JobPostingList />} />} />
<Route path="/job-posting-new/:id" element={<ProtectedRoute element={<JobPostingEdit />} />} />
<Route path="/job-posting-edit/:id" element={<ProtectedRoute element={<JobPostingEdit />} />} />
<Route path="/opportunities" element={<ProtectedRoute element={<OpportunityList />} />} />
<Route path="/opportunity-details" element={<ProtectedRoute element={<OpportunityDetails />} />} />
<Route path="/opportunity-details/:id" element={<ProtectedRoute element={<OpportunityDetails />} />} />
<Route path="/reports" element={<ProtectedRoute element={<Reports />} />} />
<Route path="/reports/:reportType/:referenceDate?" element={<ProtectedRoute element={<Reports />} />} />
</Routes>
Converting the Class Components to Functional Components
When you are first learning to develop React online, you see two different kinds of React components out there. At first glance, honestly, class components were initially easier to learn than functional. React doesn’t typically live by itself: there is a back end. So many of the back-end references of “build an application using X MVC and React” (Laravel with React, Rails with React, Django with React) are written using class components instead of the modern functional components. That is what I learned and it just worked, so I stayed with my class components.
Later, though all of my complex components with forms were still using the class definition, I started writing the newer static pages (no back-end server data) as functional components. One of the imported components ( the editor component) was already a functional component.
The modern functional components enable new features like the useEffect
, memoization, and other features that are necessary for the fetch
to properly work and for the back-end data-intensive components to properly render the data. So, I converted every single one of my components over to the modern functional component structure.
Environment Settings
Getting closer to deploying the application as a container to run in AWS, you start thinking about the differences between development and production environments. Having to create a few new constants, it was time to start using the correct import structure.
Static constants are all stored in (for now):
// frontend/src/constants/index.js
const BASE_API_URL = process.env.REACT_APP_API_URL;
export const JOB_OPPORTUNITY_API_URL = BASE_API_URL + 'email_opportunity/';
export const JOB_SITE_API_URL = BASE_API_URL + 'job_site/';
export const JOB_POSTING_API_URL = BASE_API_URL + 'job_posting/';
export const DASHBOARD_API_URL = BASE_API_URL + 'dashboard/';
export const REPORT_API_URL = BASE_API_URL + 'report/';
export const TOKEN_REFRESH_API_URL = BASE_API_URL + 'token/refresh/';
export const formatInputFieldDateTime = (originalDateTime) => {
if (originalDateTime === null) return null;
// Properly format date-time
// Going into the database, the time zone is saved.
// The front end widget does not need a time zone, nor the 'T'.
// From: 2024-09-02T14:19:00-07:00
// To: 2024-09-02 14:19:00 (no time zones)
var originalDtArr = originalDateTime.split('T')
var originalDtTimeArr = originalDtArr[1].split('-')
return originalDtArr[0] + ' ' + originalDtTimeArr[0]
}
export const formatDisplayDateTime = (rawDate) => {
if (rawDate === null) return null;
// From a code perspective, I would love to live with the default:
// const theDate = new Date(rawDate).toLocaleString('en-US')
// 9/5/2024, 5:42:00 PM
// but... you can't easily turn off the seconds!
// return: Tuesday, Sep 10, 2024, 5:42 AM
const theDate = new Date(rawDate).toLocaleString('en-US', {
weekday: 'long',
year: 'numeric',
month: 'short',
day: 'numeric',
hour12:true,
hour:'numeric',
minute:'numeric'})
return theDate
}
// return: Sep 10, 2024
export const formatDisplayDate = (rawDate) => {
if (rawDate === undefined || rawDate === null) return null;
const theDate = new Date(rawDate).toLocaleString('en-US', {
year: 'numeric',
month: 'short',
day: 'numeric'})
return theDate
}
Yes, those utility functions really belong in something separate from the src/constants/index.js file but that is to be fixed at another time. The constants file imports the process.env values, which are defined as:
/.env
REACT_APP_API_URL=http://localhost:8000/api/
REACT_APP_ENABLE_AUTH=true
REACT_APP_ENV=development # This can be 'development' or 'production'
This is the file that I can either manually change or have docker adjust for setting which environment to use.
Final Thoughts
This sprint has been the most challenging yet. However, I persevered, learning many new react features and the reasons why, long term, all components should be implemented with functional components. It has enabled me to have multiple users, so that my test data doesn’t mingle with my actual job hunt data and can enable me to have a third sample user data. This release really readies the code for a truly production-worthy application.