ASP.NET Core and React: Building Dynamic and Interactive Frontends
Combining Microsoft's Backend Power with Facebook's UI Library
The combination of ASP.NET Core and React has emerged as one of the most powerful technology stacks for building modern web applications. This pairing brings together Microsoft's robust backend framework with Facebook's flexible UI library, enabling developers to create responsive, scalable applications that provide exceptional user experiences without sacrificing performance or maintainability.
In today's digital landscape, users expect web applications to be fast, intuitive, and responsive across all devices. The traditional approach of server-rendered pages with occasional JavaScript enhancements no longer meets these expectations. Modern web development has shifted toward rich client-side applications backed by powerful APIs—and the ASP.NET Core + React stack sits at the forefront of this evolution.
This article will guide you through everything you need to know about building modern web applications with ASP.NET Core and React—from understanding the fundamental concepts, setting up your development environment, creating your first application, to implementing advanced features that will elevate your user interfaces to the next level.
Why Choose ASP.NET Core and React?
Before diving into implementation details, let's explore why this particular technology combination has become so popular among developers and organizations worldwide.
The Power of ASP.NET Core
ASP.NET Core represents Microsoft's reimagining of its web development platform. Unlike its predecessor, ASP.NET Core is:
Cross-platform: It runs on Windows, macOS, and Linux, freeing developers from operating system constraints.
High-performance: It consistently ranks among the fastest web frameworks in TechEmpower benchmarks, often outperforming Express.js, Django, and Laravel in raw throughput.
Modern and modular: Built from the ground up with modern development practices in mind, it employs a modular approach where you only include what you need.
Open-source: The framework is developed openly on GitHub, with contributions from both Microsoft and the community.
Cloud-ready: It's designed with cloud deployment in mind, with excellent support for containerization and microservices architectures.
When paired with React, ASP.NET Core typically serves as the backend API service, handling authentication, business logic, data access, and security—areas where Microsoft's enterprise expertise shines.
The Flexibility of React
React, developed and maintained by Facebook (now Meta), offers a different approach to building user interfaces than traditional frameworks:
Component-based architecture: Everything in React is a component, making code more reusable and easier to reason about.
Virtual DOM: React's virtual DOM implementation efficiently updates only what needs to change, resulting in superior performance.
One-way data flow: This makes applications more predictable and easier to debug compared to two-way binding approaches.
JSX syntax: The JSX syntax extension allows for intuitive component composition that feels natural to developers.
Massive ecosystem: With thousands of libraries and tools, React has solutions for almost any frontend challenge.
Lightweight core: Unlike more opinionated frameworks, React focuses on the view layer, giving developers flexibility in how they structure applications.
React's learning curve is relatively gentle, especially for developers already familiar with JavaScript. Its focus on functional programming concepts and immutability aligns well with modern development trends.
Why They Work Well Together
The ASP.NET Core + React combination offers several advantages:
Clear separation of concerns: Backend and frontend have distinct responsibilities, making codebases more maintainable.
Independent scaling: UI and API layers can be scaled independently based on demand.
Specialized developer roles: Teams can have dedicated frontend and backend specialists working in parallel.
TypeScript integration: Both ecosystems have excellent TypeScript support, providing type safety across the stack.
Enterprise-grade security: ASP.NET Core's robust security features protect sensitive operations while React handles presentation.
Best-of-breed approach: Rather than a monolithic framework, you're using best-in-class tools for each layer.
This pairing is particularly appealing to organizations with existing investments in Microsoft technologies that want to modernize their user interfaces without abandoning their backend expertise.
Setting Up Your Development Environment
To build applications with ASP.NET Core and React, you'll need to set up a development environment that supports both technologies. Here's what you'll need:
Prerequisites
.NET 8 SDK (or latest version): Download from https://dotnet.microsoft.com/download
Node.js and npm: Download from https://nodejs.org/ (use the LTS version)
Code Editor:
Visual Studio 2022: Best for Windows users who prefer an all-in-one IDE
Visual Studio Code: Excellent cross-platform editor with great support for both ASP.NET Core and React
Git: For version control (download from https://git-scm.com/)
Recommended VS Code Extensions
If you're using Visual Studio Code, these extensions will enhance your development experience:
C# for Visual Studio Code (powered by OmniSharp)
ESLint
Prettier - Code formatter
ES7+ React/Redux/React-Native snippets
Debugger for Chrome/Edge
Creating a New Project
Microsoft provides a template that sets up ASP.NET Core with React. To create a new project, open a terminal/command prompt and run:
bash
dotnet new react -o MyReactApp
This creates a new directory called MyReactApp
with an ASP.NET Core backend and a React frontend pre-configured to work together.
Alternatively, for more control over the setup, you can create separate projects:
bash
# Create ASP.NET Core Web API
dotnet new webapi -o MyReactApp.Api
# Create React app using Create React App
npx create-react-app my-react-app --template typescript
For this article, we'll use the integrated template approach for simplicity.
Understanding the Project Structure
The template-generated project has a specific structure designed to keep the backend and frontend code separate but coordinated. Let's explore the key parts:
Backend Structure
The root directory contains the ASP.NET Core project:
Controllers/: Contains API controllers that serve data to the React application
Program.cs: Entry point for the ASP.NET Core application
appsettings.json: Configuration settings
MyReactApp.csproj: Project file with dependencies and configurations
Frontend Structure
The React application resides in the ClientApp
directory:
src/components/: Contains React components
src/App.js: Main React component that serves as the entry point
src/index.js: JavaScript entry point
public/: Static assets like HTML, images, and the manifest
package.json: NPM dependencies and scripts
Configuration Files
The template sets up important configuration to make the two parts work together:
setupProxy.js: Configures the development proxy to forward API requests from React to ASP.NET Core
.eslintrc.json: Linting rules for JavaScript/TypeScript
tsconfig.json: TypeScript configuration (if using TypeScript)
Creating Your First API Endpoint
Now that we understand the project structure, let's create a simple API endpoint that our React application can consume. We'll build a basic "products" API.
1. Create a Model
First, create a Models
folder in the root directory and add a Product.cs
file:
csharp
namespace MyReactApp.Models
{
public class Product
{
public int Id { get; set; }
public string Name { get; set; } = string.Empty;
public string Description { get; set; } = string.Empty;
public decimal Price { get; set; }
public int StockQuantity { get; set; }
}
}
2. Create a Controller
Now, create a ProductsController.cs
file in the Controllers
directory:
csharp
using Microsoft.AspNetCore.Mvc;
using MyReactApp.Models;
namespace MyReactApp.Controllers
{
[ApiController]
[Route("api/[controller]")]
public class ProductsController : ControllerBase
{
private static readonly List<Product> _products = new List<Product>
{
new Product { Id = 1, Name = "Surface Laptop 4", Description = "Thin and light laptop", Price = 999.99m, StockQuantity = 15 },
new Product { Id = 2, Name = "Xbox Series X", Description = "Next-gen gaming console", Price = 499.99m, StockQuantity = 5 },
new Product { Id = 3, Name = "Surface Pro 8", Description = "Versatile 2-in-1 tablet", Price = 1099.99m, StockQuantity = 10 }
};
[HttpGet]
public ActionResult<IEnumerable<Product>> GetAll()
{
return Ok(_products);
}
[HttpGet("{id}")]
public ActionResult<Product> GetById(int id)
{
var product = _products.FirstOrDefault(p => p.Id == id);
if (product == null)
return NotFound();
return Ok(product);
}
[HttpPost]
public ActionResult<Product> Create(Product product)
{
// In a real app, we would persist to a database
product.Id = _products.Max(p => p.Id) + 1;
_products.Add(product);
return CreatedAtAction(nameof(GetById), new { id = product.Id }, product);
}
}
}
This controller provides endpoints to list all products, get a product by ID, and create new products. In a real application, you would use a database instead of an in-memory list.
Building the React Frontend
Now that we have our API, let's create React components to display and interact with our products data.
1. Create a Product Service
First, let's create a service to handle API calls. In the ClientApp/src
directory, create a services
folder with a file named productService.js
:
javascript
// ClientApp/src/services/productService.js
export const productService = {
getAllProducts: async () => {
const response = await fetch('api/products');
if (!response.ok) {
throw new Error('Network response was not ok');
}
return response.json();
},
getProductById: async (id) => {
const response = await fetch(`api/products/${id}`);
if (!response.ok) {
throw new Error('Network response was not ok');
}
return response.json();
},
createProduct: async (product) => {
const response = await fetch('api/products', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(product),
});
if (!response.ok) {
throw new Error('Network response was not ok');
}
return response.json();
}
};
2. Create React Components
Next, let's create components to display the product data. In the ClientApp/src/components
directory, create a products
folder with the following files:
ProductList.js:
javascript
import React, { useState, useEffect } from 'react';
import { productService } from '../../services/productService';
import { Link } from 'react-router-dom';
import './ProductList.css';
export function ProductList() {
const [products, setProducts] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
const fetchProducts = async () => {
try {
const data = await productService.getAllProducts();
setProducts(data);
setLoading(false);
} catch (error) {
setError('Failed to fetch products. Please try again later.');
setLoading(false);
}
};
fetchProducts();
}, []);
if (loading) return <p>Loading...</p>;
if (error) return <p className="error-message">{error}</p>;
return (
<div className="product-list-container">
<h1>Product Catalog</h1>
<div className="product-grid">
{products.map(product => (
<div key={product.id} className="product-card">
<h2>{product.name}</h2>
<p>{product.description}</p>
<p className="price">${product.price.toFixed(2)}</p>
<p className="stock">In stock: {product.stockQuantity}</p>
<Link to={`/product/${product.id}`} className="view-button">
View Details
</Link>
</div>
))}
</div>
</div>
);
}
ProductDetail.js:
javascript
import React, { useState, useEffect } from 'react';
import { useParams, useNavigate } from 'react-router-dom';
import { productService } from '../../services/productService';
import './ProductDetail.css';
export function ProductDetail() {
const { id } = useParams();
const navigate = useNavigate();
const [product, setProduct] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
const fetchProduct = async () => {
try {
const data = await productService.getProductById(id);
setProduct(data);
setLoading(false);
} catch (error) {
setError('Failed to fetch product details. Please try again later.');
setLoading(false);
}
};
fetchProduct();
}, [id]);
if (loading) return <p>Loading...</p>;
if (error) return <p className="error-message">{error}</p>;
if (!product) return <p>Product not found</p>;
return (
<div className="product-detail-container">
<button className="back-button" onClick={() => navigate(-1)}>
← Back to Products
</button>
<div className="product-detail-card">
<h1>{product.name}</h1>
<div className="product-meta">
<span className="product-price">${product.price.toFixed(2)}</span>
<span className="product-stock">
{product.stockQuantity > 0 ? `In stock (${product.stockQuantity})` : 'Out of stock'}
</span>
</div>
<p className="product-description">{product.description}</p>
<button className="add-to-cart-button">Add to Cart</button>
</div>
</div>
);
}
ProductForm.js:
javascript
import React, { useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { productService } from '../../services/productService';
import './ProductForm.css';
export function ProductForm() {
const navigate = useNavigate();
const [formData, setFormData] = useState({
name: '',
description: '',
price: '',
stockQuantity: '',
});
const [errors, setErrors] = useState({});
const [submitting, setSubmitting] = useState(false);
const handleChange = (e) => {
const { name, value } = e.target;
setFormData(prev => ({
...prev,
[name]: value
}));
// Clear the error for this field when the user changes it
if (errors[name]) {
setErrors(prev => ({
...prev,
[name]: null
}));
}
};
const validateForm = () => {
const newErrors = {};
if (!formData.name.trim()) {
newErrors.name = 'Product name is required';
}
if (!formData.description.trim()) {
newErrors.description = 'Description is required';
}
if (!formData.price || isNaN(formData.price) || parseFloat(formData.price) <= 0) {
newErrors.price = 'Please enter a valid price greater than zero';
}
if (!formData.stockQuantity || isNaN(formData.stockQuantity) || parseInt(formData.stockQuantity) < 0) {
newErrors.stockQuantity = 'Please enter a valid stock quantity';
}
setErrors(newErrors);
return Object.keys(newErrors).length === 0;
};
const handleSubmit = async (e) => {
e.preventDefault();
if (!validateForm()) {
return;
}
try {
setSubmitting(true);
const productToCreate = {
name: formData.name,
description: formData.description,
price: parseFloat(formData.price),
stockQuantity: parseInt(formData.stockQuantity)
};
await productService.createProduct(productToCreate);
navigate('/products');
} catch (error) {
setErrors({ submit: 'Failed to create product. Please try again.' });
} finally {
setSubmitting(false);
}
};
return (
<div className="product-form-container">
<h1>Add New Product</h1>
<form onSubmit={handleSubmit} className="product-form">
<div className="form-group">
<label htmlFor="name">Product Name</label>
<input
type="text"
id="name"
name="name"
value={formData.name}
onChange={handleChange}
className={errors.name ? 'error' : ''}
/>
{errors.name && <div className="error-text">{errors.name}</div>}
</div>
<div className="form-group">
<label htmlFor="description">Description</label>
<textarea
id="description"
name="description"
value={formData.description}
onChange={handleChange}
rows="4"
className={errors.description ? 'error' : ''}
/>
{errors.description && <div className="error-text">{errors.description}</div>}
</div>
<div className="form-row">
<div className="form-group">
<label htmlFor="price">Price ($)</label>
<input
type="number"
id="price"
name="price"
value={formData.price}
onChange={handleChange}
step="0.01"
min="0"
className={errors.price ? 'error' : ''}
/>
{errors.price && <div className="error-text">{errors.price}</div>}
</div>
<div className="form-group">
<label htmlFor="stockQuantity">Stock Quantity</label>
<input
type="number"
id="stockQuantity"
name="stockQuantity"
value={formData.stockQuantity}
onChange={handleChange}
min="0"
className={errors.stockQuantity ? 'error' : ''}
/>
{errors.stockQuantity && <div className="error-text">{errors.stockQuantity}</div>}
</div>
</div>
{errors.submit && <div className="error-banner">{errors.submit}</div>}
<div className="form-actions">
<button type="button" className="cancel-button" onClick={() => navigate('/products')}>
Cancel
</button>
<button type="submit" className="submit-button" disabled={submitting}>
{submitting ? 'Creating...' : 'Create Product'}
</button>
</div>
</form>
</div>
);
}
3. Update the Routes
Now, update the ClientApp/src/App.js
file to include routes for your product components:
javascript
import React from 'react';
import { Route, Routes } from 'react-router-dom';
import { Layout } from './components/Layout';
import { Home } from './components/Home';
import { ProductList } from './components/products/ProductList';
import { ProductDetail } from './components/products/ProductDetail';
import { ProductForm } from './components/products/ProductForm';
import './custom.css';
export default function App() {
return (
<Layout>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/products" element={<ProductList />} />
<Route path="/product/:id" element={<ProductDetail />} />
<Route path="/products/new" element={<ProductForm />} />
</Routes>
</Layout>
);
}
4. Add Some Basic CSS
To make our components look better, let's add some CSS. Create the following CSS files:
ProductList.css:
css
.product-list-container {
padding: 20px;
}
.product-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
grid-gap: 20px;
margin-top: 20px;
}
.product-card {
border: 1px solid #ddd;
border-radius: 8px;
padding: 20px;
transition: transform 0.2s, box-shadow 0.2s;
}
.product-card:hover {
transform: translateY(-5px);
box-shadow: 0 10px 20px rgba(0, 0, 0, 0.1);
}
.product-card h2 {
margin-top: 0;
color: #333;
}
.price {
font-size: 1.2rem;
font-weight: bold;
color: #e53935;
}
.stock {
color: #555;
font-size: 0.9rem;
}
.view-button {
display: inline-block;
margin-top: 10px;
padding: 8px 16px;
background-color: #0078d4;
color: white;
text-decoration: none;
border-radius: 4px;
transition: background-color 0.2s;
}
.view-button:hover {
background-color: #005a9e;
}
.error-message {
color: #e53935;
padding: 10px;
background-color: #ffebee;
border-radius: 4px;
}
Adding State Management with React Context
As our application grows, managing state becomes more complex. Let's implement a basic state management solution using React Context API. This approach is lighter than Redux and suitable for small to medium-sized applications.
Creating a Products Context
First, create a contexts
folder in the ClientApp/src
directory and add a file named ProductsContext.js
:
javascript
import React, { createContext, useContext, useReducer, useEffect } from 'react';
import { productService } from '../services/productService';
// Initial state
const initialState = {
products: [],
loading: false,
error: null,
selectedProduct: null
};
// Create context
const ProductsContext = createContext(initialState);
// Actions
const ACTIONS = {
FETCH_PRODUCTS_REQUEST: 'FETCH_PRODUCTS_REQUEST',
FETCH_PRODUCTS_SUCCESS: 'FETCH_PRODUCTS_SUCCESS',
FETCH_PRODUCTS_FAILURE: 'FETCH_PRODUCTS_FAILURE',
SELECT_PRODUCT: 'SELECT_PRODUCT',
ADD_PRODUCT: 'ADD_PRODUCT'
};
// Reducer function
function productsReducer(state, action) {
switch (action.type) {
case ACTIONS.FETCH_PRODUCTS_REQUEST:
return {
...state,
loading: true,
error: null
};
case ACTIONS.FETCH_PRODUCTS_SUCCESS:
return {
...state,
loading: false,
products: action.payload
};
case ACTIONS.FETCH_PRODUCTS_FAILURE:
return {
...state,
loading: false,
error: action.payload
};
case ACTIONS.SELECT_PRODUCT:
return {
...state,
selectedProduct: action.payload
};
case ACTIONS.ADD_PRODUCT:
return {
...state,
products: [...state.products, action.payload]
};
default:
return state;
}
}
// Provider component
export function ProductsProvider({ children }) {
const [state, dispatch] = useReducer(productsReducer, initialState);
// Load products when the provider mounts
useEffect(() => {
const fetchProducts = async () => {
dispatch({ type: ACTIONS.FETCH_PRODUCTS_REQUEST });
try {
const products = await productService.getAllProducts();
dispatch({
type: ACTIONS.FETCH_PRODUCTS_SUCCESS,
payload: products
});
} catch (error) {
dispatch({
type: ACTIONS.FETCH_PRODUCTS_FAILURE,
payload: error.message
});
}
};
fetchProducts();
}, []);
// Actions
const selectProduct = async (id) => {
dispatch({ type: ACTIONS.FETCH_PRODUCTS_REQUEST });
try {
const product = await productService.getProductById(id);
dispatch({
type: ACTIONS.SELECT_PRODUCT,
payload: product
});
return product;
} catch (error) {
dispatch({
type: ACTIONS.FETCH_PRODUCTS_FAILURE,
payload: error.message
});
throw error;
}
};
const addProduct = async (product) => {
dispatch({ type: ACTIONS.FETCH_PRODUCTS_REQUEST });
try {
const newProduct = await productService.createProduct(product);
dispatch({
type: ACTIONS.ADD_PRODUCT,
payload: newProduct
});
return newProduct;
} catch (error) {
dispatch({
type: ACTIONS.FETCH_PRODUCTS_FAILURE,
payload: error.message
});
throw error;
}
};
return (
<ProductsContext.Provider value={{
...state,
selectProduct,
addProduct
}}>
{children}
</ProductsContext.Provider>
);
}
// Custom hook for using the context
export function useProducts() {
const context = useContext(ProductsContext);
if (context === undefined) {
throw new Error('useProducts must be used within a ProductsProvider');
}
return context;
}
Update App.js to Include the Provider
Now, update your App.js
to wrap your components with the ProductsProvider
:
javascript
import React from 'react';
import { Route, Routes } from 'react-router-dom';
import { Layout } from './components/Layout';
import { Home } from './components/Home';
import { ProductList } from './components/products/ProductList';
import { ProductDetail } from './components/products/ProductDetail';
import { ProductForm } from './components/products/ProductForm';
import { ProductsProvider } from './contexts/ProductsContext';
import './custom.css';
export default function App() {
return (
<ProductsProvider>
<Layout>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/products" element={<ProductList />} />
<Route path="/product/:id" element={<ProductDetail />} />
<Route path="/products/new" element={<ProductForm />} />
</Routes>
</Layout>
</ProductsProvider>
);
}
Update Components to Use Context
Now, update the ProductList.js
component to use the context:
javascript
import React from 'react';
import { Link } from 'react-router-dom';
import { useProducts } from '../../contexts/ProductsContext';
import './ProductList.css';
export function ProductList() {
const { products, loading, error } = useProducts();
if (loading) return <p>Loading...</p>;
if (error) return <p className="error-message">{error}</p>;
return (
<div className="product-list-container">
<div className="product-header">
<h1>Product Catalog</h1>
<Link to="/products/new" className="add-product-button">
Add New Product
</Link>
</div>
<div className="product-grid">
{products.map(product => (
<div key={product.id} className="product-card">
<h2>{product.name}</h2>
<p>{product.description}</p>
<p className="price">${product.price.toFixed(2)}</p>
<p className="stock">In stock: {product.stockQuantity}</p>
<Link to={`/product/${product.id}`} className="view-button">
View Details
</Link>
</div>
))}
</div>
</div>
);
}
Similarly, update ProductDetail.js
and ProductForm.js
to use the context instead of direct API calls.
Implementing Authentication
Most real-world applications require authentication. Let's implement token-based authentication using ASP.NET Core Identity and JWT.
Backend Authentication Setup
First, let's set up authentication on the backend:
Add the necessary NuGet packages:
bash
dotnet add package Microsoft.AspNetCore.Authentication.JwtBearer
dotnet add package Microsoft.AspNetCore.Identity.EntityFrameworkCore
dotnet add package Microsoft.EntityFrameworkCore.SqlServer
dotnet add package Microsoft.EntityFrameworkCore.Tools
Create
ApplicationUser.cs
in a newModels
folder:
csharp
using Microsoft.AspNetCore.Identity;
namespace MyReactApp.Models
{
public class ApplicationUser : IdentityUser
{
public string FirstName { get; set; } = string.Empty;
public string LastName { get; set; } = string.Empty;
}
}
Create
ApplicationDbContext.cs
in a newData
folder:
csharp
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore;
using MyReactApp.Models;
namespace MyReactApp.Data
{
public class ApplicationDbContext : IdentityDbContext<ApplicationUser>
{
public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options)
: base(options)
{
}
public DbSet<Product> Products { get; set; } = null!;
}
}
Create authentication models in the
Models
folder:
csharp
// Models/LoginModel.cs
namespace MyReactApp.Models
{
public class LoginModel
{
public string Email { get; set; } = string.Empty;
public string Password { get; set; } = string.Empty;
}
}
// Models/RegisterModel.cs
namespace MyReactApp.Models
{
public class RegisterModel
{
public string Email { get; set; } = string.Empty;
public string Password { get; set; } = string.Empty;
public string FirstName { get; set; } = string.Empty;
public string LastName { get; set; } = string.Empty;
}
}
// Models/AuthResponse.cs
namespace MyReactApp.Models
{
public class AuthResponse
{
public string Token { get; set; } = string.Empty;
public string Email { get; set; } = string.Empty;
public string FirstName { get; set; } = string.Empty;
public string LastName { get; set; } = string.Empty;
}
}
Create an
AuthController.cs
in the Controllers folder:
csharp
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Configuration;
using Microsoft.IdentityModel.Tokens;
using MyReactApp.Models;
using System;
using System.Collections.Generic;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Text;
using System.Threading.Tasks;
namespace MyReactApp.Controllers
{
[Route("api/[controller]")]
[ApiController]
public class AuthController : ControllerBase
{
private readonly UserManager<ApplicationUser> _userManager;
private readonly SignInManager<ApplicationUser> _signInManager;
private readonly IConfiguration _configuration;
public AuthController(
UserManager<ApplicationUser> userManager,
SignInManager<ApplicationUser> signInManager,
IConfiguration configuration)
{
_userManager = userManager;
_signInManager = signInManager;
_configuration = configuration;
}
[HttpPost("register")]
public async Task<IActionResult> Register(RegisterModel model)
{
var user = new ApplicationUser
{
UserName = model.Email,
Email = model.Email,
FirstName = model.FirstName,
LastName = model.LastName
};
var result = await _userManager.CreateAsync(user, model.Password);
if (result.Succeeded)
{
return Ok(new { message = "User registered successfully" });
}
return BadRequest(result.Errors);
}
[HttpPost("login")]
public async Task<IActionResult> Login(LoginModel model)
{
var result = await _signInManager.PasswordSignInAsync(model.Email, model.Password, false, false);
if (result.Succeeded)
{
var user = await _userManager.FindByEmailAsync(model.Email);
var token = GenerateJwtToken(user);
return Ok(new AuthResponse
{
Token = token,
Email = user.Email,
FirstName = user.FirstName,
LastName = user.LastName
});
}
return Unauthorized(new { message = "Invalid login attempt" });
}
private string GenerateJwtToken(ApplicationUser user)
{
var claims = new List<Claim>
{
new Claim(JwtRegisteredClaimNames.Sub, user.Email),
new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()),
new Claim(ClaimTypes.NameIdentifier, user.Id)
};
var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_configuration["Jwt:Key"]));
var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
var expires = DateTime.Now.AddDays(Convert.ToDouble(_configuration["Jwt:ExpireDays"]));
var token = new JwtSecurityToken(
_configuration["Jwt:Issuer"],
_configuration["Jwt:Issuer"],
claims,
expires: expires,
signingCredentials: creds
);
return new JwtSecurityTokenHandler().WriteToken(token);
}
}
}
Update
appsettings.json
to include JWT settings:
json
{
"ConnectionStrings": {
"DefaultConnection": "Server=(localdb)\\mssqllocaldb;Database=MyReactApp;Trusted_Connection=True;MultipleActiveResultSets=true"
},
"Jwt": {
"Key": "ThisIsMySecretKeyForAuthenticationOfApplicationUsers1234567890",
"Issuer": "MyReactApp",
"ExpireDays": 30
},
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information"
}
},
"AllowedHosts": "*"
}
Update
Program.cs
to configure authentication and database:
csharp
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using Microsoft.IdentityModel.Tokens;
using MyReactApp.Data;
using MyReactApp.Models;
using System.Text;
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
var connectionString = builder.Configuration.GetConnectionString("DefaultConnection");
builder.Services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(connectionString));
builder.Services.AddIdentity<ApplicationUser, IdentityRole>()
.AddEntityFrameworkStores<ApplicationDbContext>()
.AddDefaultTokenProviders();
// Configure JWT authentication
var jwtSettings = builder.Configuration.GetSection("Jwt");
var key = Encoding.ASCII.GetBytes(jwtSettings["Key"]);
builder.Services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(options =>
{
options.RequireHttpsMetadata = false;
options.SaveToken = true;
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(key),
ValidateIssuer = true,
ValidateAudience = true,
ValidIssuer = jwtSettings["Issuer"],
ValidAudience = jwtSettings["Issuer"],
ClockSkew = TimeSpan.Zero
};
});
builder.Services.AddControllers();
var app = builder.Build();
// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.MapControllerRoute(
name: "default",
pattern: "{controller}/{action=Index}/{id?}");
app.MapFallbackToFile("index.html");
app.Run();
Update
ProductsController.cs
to require authentication:
csharp
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using MyReactApp.Models;
namespace MyReactApp.Controllers
{
[ApiController]
[Route("api/[controller]")]
[Authorize] // Add this attribute to require authentication
public class ProductsController : ControllerBase
{
// ... existing code
}
}
Frontend Authentication Implementation
Now, let's implement authentication on the React side:
Create an authentication context in
ClientApp/src/contexts/AuthContext.js
:
javascript
import React, { createContext, useContext, useReducer, useEffect } from 'react';
// Initial state
const initialState = {
isAuthenticated: false,
user: null,
token: null,
loading: false,
error: null
};
// Load state from localStorage
const loadAuthState = () => {
try {
const token = localStorage.getItem('token');
const user = JSON.parse(localStorage.getItem('user'));
if (token && user) {
return {
...initialState,
isAuthenticated: true,
token,
user
};
}
return initialState;
} catch (error) {
return initialState;
}
};
// Actions
const ACTIONS = {
LOGIN_REQUEST: 'LOGIN_REQUEST',
LOGIN_SUCCESS: 'LOGIN_SUCCESS',
LOGIN_FAILURE: 'LOGIN_FAILURE',
REGISTER_REQUEST: 'REGISTER_REQUEST',
REGISTER_SUCCESS: 'REGISTER_SUCCESS',
REGISTER_FAILURE: 'REGISTER_FAILURE',
LOGOUT: 'LOGOUT'
};
// Reducer
function authReducer(state, action) {
switch (action.type) {
case ACTIONS.LOGIN_REQUEST:
case ACTIONS.REGISTER_REQUEST:
return {
...state,
loading: true,
error: null
};
case ACTIONS.LOGIN_SUCCESS:
return {
...state,
isAuthenticated: true,
user: action.payload.user,
token: action.payload.token,
loading: false,
error: null
};
case ACTIONS.REGISTER_SUCCESS:
return {
...state,
loading: false,
error: null
};
case ACTIONS.LOGIN_FAILURE:
case ACTIONS.REGISTER_FAILURE:
return {
...state,
loading: false,
error: action.payload
};
case ACTIONS.LOGOUT:
return {
...initialState
};
default:
return state;
}
}
// Create context
const AuthContext = createContext();
// Provider component
export function AuthProvider({ children }) {
const [state, dispatch] = useReducer(authReducer, initialState, loadAuthState);
// Save authentication state to localStorage when it changes
useEffect(() => {
if (state.isAuthenticated) {
localStorage.setItem('token', state.token);
localStorage.setItem('user', JSON.stringify(state.user));
} else {
localStorage.removeItem('token');
localStorage.removeItem('user');
}
}, [state.isAuthenticated, state.token, state.user]);
// Login function
const login = async (email, password) => {
dispatch({ type: ACTIONS.LOGIN_REQUEST });
try {
const response = await fetch('api/auth/login', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ email, password })
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.message || 'Invalid credentials');
}
const data = await response.json();
dispatch({
type: ACTIONS.LOGIN_SUCCESS,
payload: {
user: {
email: data.email,
firstName: data.firstName,
lastName: data.lastName
},
token: data.token
}
});
return data;
} catch (error) {
dispatch({
type: ACTIONS.LOGIN_FAILURE,
payload: error.message
});
throw error;
}
};
// Register function
const register = async (userData) => {
dispatch({ type: ACTIONS.REGISTER_REQUEST });
try {
const response = await fetch('api/auth/register', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(userData)
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.message || 'Registration failed');
}
dispatch({ type: ACTIONS.REGISTER_SUCCESS });
return await response.json();
} catch (error) {
dispatch({
type: ACTIONS.REGISTER_FAILURE,
payload: error.message
});
throw error;
}
};
// Logout function
const logout = () => {
dispatch({ type: ACTIONS.LOGOUT });
};
return (
<AuthContext.Provider value={{
...state,
login,
register,
logout
}}>
{children}
</AuthContext.Provider>
);
}
// Custom hook
export function useAuth() {
const context = useContext(AuthContext);
if (context === undefined) {
throw new Error('useAuth must be used within an AuthProvider');
}
return context;
}
Update the API service to include authentication token:
javascript
// ClientApp/src/services/productService.js
export const productService = {
getAllProducts: async (token) => {
const response = await fetch('api/products', {
headers: {
'Authorization': `Bearer ${token}`
}
});
if (!response.ok) {
throw new Error('Network response was not ok');
}
return response.json();
},
// Update other methods to include the token in headers
// ...
};
Create login and register components:
javascript
// ClientApp/src/components/auth/Login.js
import React, { useState } from 'react';
import { useNavigate, Link } from 'react-router-dom';
import { useAuth } from '../../contexts/AuthContext';
import './Auth.css';
export function Login() {
const navigate = useNavigate();
const { login, error: authError, loading } = useAuth();
const [credentials, setCredentials] = useState({
email: '',
password: ''
});
const [errors, setErrors] = useState({});
const handleChange = (e) => {
const { name, value } = e.target;
setCredentials(prev => ({
...prev,
[name]: value
}));
// Clear the error for this field
if (errors[name]) {
setErrors(prev => ({
...prev,
[name]: null
}));
}
};
const validateForm = () => {
const newErrors = {};
if (!credentials.email.trim()) {
newErrors.email = 'Email is required';
} else if (!/\S+@\S+\.\S+/.test(credentials.email)) {
newErrors.email = 'Email is invalid';
}
if (!credentials.password) {
newErrors.password = 'Password is required';
}
setErrors(newErrors);
return Object.keys(newErrors).length === 0;
};
const handleSubmit = async (e) => {
e.preventDefault();
if (!validateForm()) {
return;
}
try {
await login(credentials.email, credentials.password);
navigate('/products');
} catch (error) {
// The error is already handled in the auth context
console.error('Login failed', error);
}
};
return (
<div className="auth-container">
<div className="auth-card">
<h1>Log In</h1>
<form onSubmit={handleSubmit} className="auth-form">
{authError && <div className="error-banner">{authError}</div>}
<div className="form-group">
<label htmlFor="email">Email</label>
<input
type="email"
id="email"
name="email"
value={credentials.email}
onChange={handleChange}
className={errors.email ? 'error' : ''}
/>
{errors.email && <div className="error-text">{errors.email}</div>}
</div>
<div className="form-group">
<label htmlFor="password">Password</label>
<input
type="password"
id="password"
name="password"
value={credentials.password}
onChange={handleChange}
className={errors.password ? 'error' : ''}
/>
{errors.password && <div className="error-text">{errors.password}</div>}
</div>
<button type="submit" className="auth-button" disabled={loading}>
{loading ? 'Logging in...' : 'Log In'}
</button>
</form>
<div className="auth-footer">
Don't have an account? <Link to="/register">Register</Link>
</div>
</div>
</div>
);
}
Add an Auth.css file:
css
.auth-container {
display: flex;
justify-content: center;
align-items: center;
min-height: 80vh;
padding: 20px;
}
.auth-card {
width: 100%;
max-width: 450px;
padding: 30px;
background-color: white;
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
.auth-card h1 {
margin-top: 0;
margin-bottom: 20px;
text-align: center;
color: #333;
}
.auth-form {
display: flex;
flex-direction: column;
}
.form-group {
margin-bottom: 20px;
}
.form-group label {
display: block;
margin-bottom: 5px;
font-weight: 500;
color: #333;
}
.form-group input {
width: 100%;
padding: 10px;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 16px;
}
.form-group input.error {
border-color: #e53935;
}
.error-text {
color: #e53935;
font-size: 14px;
margin-top: 5px;
}
.error-banner {
background-color: #ffebee;
color: #e53935;
padding: 10px;
border-radius: 4px;
margin-bottom: 20px;
text-align: center;
}
.auth-button {
padding: 12px;
background-color: #0078d4;
color: white;
border: none;
border-radius: 4px;
font-size: 16px;
cursor: pointer;
transition: background-color 0.2s;
}
.auth-button:hover {
background-color: #005a9e;
}
.auth-button:disabled {
background-color: #ccc;
cursor: not-allowed;
}
.auth-footer {
margin-top: 20px;
text-align: center;
color: #666;
}
.auth-footer a {
color: #0078d4;
text-decoration: none;
}
.auth-footer a:hover {
text-decoration: underline;
}
Update the App.js to include the AuthProvider and protected routes:
javascript
import React from 'react';
import { Route, Routes, Navigate } from 'react-router-dom';
import { Layout } from './components/Layout';
import { Home } from './components/Home';
import { ProductList } from './components/products/ProductList';
import { ProductDetail } from './components/products/ProductDetail';
import { ProductForm } from './components/products/ProductForm';
import { Login } from './components/auth/Login';
import { Register } from './components/auth/Register';
import { ProductsProvider } from './contexts/ProductsContext';
import { AuthProvider, useAuth } from './contexts/AuthContext';
import './custom.css';
// Protected route component
function ProtectedRoute({ children }) {
const { isAuthenticated } = useAuth();
if (!isAuthenticated) {
return <Navigate to="/login" />;
}
return children;
}
export default function App() {
return (
<AuthProvider>
<ProductsProvider>
<Layout>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/login" element={<Login />} />
<Route path="/register" element={<Register />} />
<Route
path="/products"
element={
<ProtectedRoute>
<ProductList />
</ProtectedRoute>
}
/>
<Route
path="/product/:id"
element={
<ProtectedRoute>
<ProductDetail />
</ProtectedRoute>
}
/>
<Route
path="/products/new"
element={
<ProtectedRoute>
<ProductForm />
</ProtectedRoute>
}
/>
</Routes>
</Layout>
</ProductsProvider>
</AuthProvider>
);
}
Update the NavMenu component to show login/logout options:
javascript
import React from 'react';
import { Link } from 'react-router-dom';
import { useAuth } from '../../contexts/AuthContext';
import './NavMenu.css';
export function NavMenu() {
const { isAuthenticated, user, logout } = useAuth();
return (
<header className="navbar">
<div className="navbar-brand">
<Link to="/">MyReactApp</Link>
</div>
<nav className="navbar-nav">
<Link to="/" className="nav-item">Home</Link>
{isAuthenticated ? (
<>
<Link to="/products" className="nav-item">Products</Link>
<div className="nav-item user-info">
<span>Welcome, {user.firstName}</span>
<button onClick={logout} className="logout-button">Logout</button>
</div>
</>
) : (
<>
<Link to="/login" className="nav-item">Login</Link>
<Link to="/register" className="nav-item">Register</Link>
</>
)}
</nav>
</header>
);
}
Advanced Features
Now let's implement some advanced features that will make your application more professional.
Form Validation with Error Handling
We've already implemented basic validation in our components. Let's create a reusable validation hook:
javascript
// ClientApp/src/hooks/useFormValidation.js
import { useState } from 'react';
export function useFormValidation(initialState, validate) {
const [values, setValues] = useState(initialState);
const [errors, setErrors] = useState({});
const [touched, setTouched] = useState({});
const handleChange = (e) => {
const { name, value } = e.target;
setValues({
...values,
[name]: value
});
// Clear the error when the user changes the field
if (errors[name]) {
setErrors({
...errors,
[name]: null
});
}
};
const handleBlur = (e) => {
const { name } = e.target;
setTouched({
...touched,
[name]: true
});
// Validate just this field on blur
const fieldErrors = validate({ [name]: values[name] });
if (fieldErrors[name]) {
setErrors({
...errors,
[name]: fieldErrors[name]
});
}
};
const validateForm = () => {
const formErrors = validate(values);
setErrors(formErrors);
return Object.keys(formErrors).length === 0;
};
const resetForm = () => {
setValues(initialState);
setErrors({});
setTouched({});
};
return {
values,
errors,
touched,
handleChange,
handleBlur,
validateForm,
resetForm
};
}
Adding Loading States and Error Handling
Let's create a component for displaying loading states and errors:
javascript
// ClientApp/src/components/common/LoadingSpinner.js
import React from 'react';
import './LoadingSpinner.css';
export function LoadingSpinner({ size = 'medium', overlay = false }) {
const spinnerClasses = `spinner spinner-${size}`;
if (overlay) {
return (
<div className="spinner-overlay">
<div className={spinnerClasses}></div>
</div>
);
}
return <div className={spinnerClasses}></div>;
}
/* ClientApp/src/components/common/LoadingSpinner.css */
.spinner {
border: 4px solid rgba(0, 0, 0, 0.1);
border-left-color: #0078d4;
border-radius: 50%;
animation: spin 1s linear infinite;
}
.spinner-small {
width: 20px;
height: 20px;
}
.spinner-medium {
width: 40px;
height: 40px;
}
.spinner-large {
width: 60px;
height: 60px;
}
.spinner-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(255, 255, 255, 0.7);
display: flex;
align-items: center;
justify-content: center;
z-index: 9999;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
Creating a Notification System
Let's implement a notification system using React Context:
javascript
// ClientApp/src/contexts/NotificationContext.js
import React, { createContext, useContext, useReducer } from 'react';
import { v4 as uuidv4 } from 'uuid';
// Initial state
const initialState = {
notifications: []
};
// Create context
const NotificationContext = createContext();
// Actions
const ACTIONS = {
ADD_NOTIFICATION: 'ADD_NOTIFICATION',
REMOVE_NOTIFICATION: 'REMOVE_NOTIFICATION'
};
// Reducer
function notificationReducer(state, action) {
switch (action.type) {
case ACTIONS.ADD_NOTIFICATION:
return {
...state,
notifications: [...state.notifications, action.payload]
};
case ACTIONS.REMOVE_NOTIFICATION:
return {
...state,
notifications: state.notifications.filter(
notification => notification.id !== action.payload
)
};
default:
return state;
}
}
// Provider component
export function NotificationProvider({ children }) {
const [state, dispatch] = useReducer(notificationReducer, initialState);
const addNotification = (notification) => {
const id = notification.id || uuidv4();
const type = notification.type || 'info';
const newNotification = {
id,
type,
message: notification.message,
autoHide: notification.autoHide !== undefined ? notification.autoHide : true,
duration: notification.duration || 5000
};
dispatch({
type: ACTIONS.ADD_NOTIFICATION,
payload: newNotification
});
if (newNotification.autoHide) {
setTimeout(() => {
dispatch({
type: ACTIONS.REMOVE_NOTIFICATION,
payload: id
});
}, newNotification.duration);
}
return id;
};
const removeNotification = (id) => {
dispatch({
type: ACTIONS.REMOVE_NOTIFICATION,
payload: id
});
};
// Helper methods
const showSuccess = (message, options = {}) => {
return addNotification({ type: 'success', message, ...options });
};
const showError = (message, options = {}) => {
return addNotification({ type: 'error', message, ...options });
};
const showInfo = (message, options = {}) => {
return addNotification({ type: 'info', message, ...options });
};
const showWarning = (message, options = {}) => {
return addNotification({ type: 'warning', message, ...options });
};
return (
<NotificationContext.Provider value={{
notifications: state.notifications,
addNotification,
removeNotification,
showSuccess,
showError,
showInfo,
showWarning
}}>
{children}
</NotificationContext.Provider>
);
}
// Custom hook
export function useNotification() {
const context = useContext(NotificationContext);
if (context === undefined) {
throw new Error('useNotification must be used within a NotificationProvider');
}
return context;
}
Now, let's create a Notification component:
javascript
// ClientApp/src/components/common/Notifications.js
import React from 'react';
import { useNotification } from '../../contexts/NotificationContext';
import './Notifications.css';
export function Notifications() {
const { notifications, removeNotification } = useNotification();
return (
<div className="notifications-container">
{notifications.map(notification => (
<div
key={notification.id}
className={`notification notification-${notification.type}`}
>
<div className="notification-message">{notification.message}</div>
<button
className="notification-close"
onClick={() => removeNotification(notification.id)}
>
×
</button>
</div>
))}
</div>
);
}
css
/* ClientApp/src/components/common/Notifications.css */
.notifications-container {
position: fixed;
top: 20px;
right: 20px;
width: 300px;
z-index: 9999;
}
.notification {
margin-bottom: 10px;
padding: 15px;
border-radius: 4px;
display: flex;
align-items: center;
justify-content: space-between;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
animation: slide-in 0.3s ease;
}
@keyframes slide-in {
from {
transform: translateX(100%);
opacity: 0;
}
to {
transform: translateX(0);
opacity: 1;
}
}
.notification-success {
background-color: #e8f5e9;
border-left: 4px solid #4caf50;
color: #2e7d32;
}
.notification-error {
background-color: #ffebee;
border-left: 4px solid #f44336;
color: #c62828;
}
.notification-info {
background-color: #e3f2fd;
border-left: 4px solid #2196f3;
color: #0d47a1;
}
.notification-warning {
background-color: #fff8e1;
border-left: 4px solid #ffc107;
color: #ff8f00;
}
.notification-message {
flex: 1;
}
.notification-close {
background: none;
border: none;
color: inherit;
font-size: 18px;
cursor: pointer;
padding: 0 0 0 10px;
}
Implementing Responsive Design
One of the advantages of React is its ability to create responsive interfaces. Let's add responsive design to our application:
css
/* ClientApp/src/custom.css (append this) */
/* Responsive design */
@media (max-width: 768px) {
.product-grid {
grid-template-columns: repeat(auto-fill, minmax(100%, 1fr));
}
.form-row {
flex-direction: column;
}
.form-group {
width: 100% !important;
margin-right: 0 !important;
}
.navbar {
flex-direction: column;
padding: 10px;
}
.navbar-nav {
margin-top: 10px;
width: 100%;
justify-content: space-between;
}
}
@media (max-width: 480px) {
.auth-card {
padding: 20px;
}
.product-detail-card {
padding: 15px;
}
.form-actions {
flex-direction: column;
}
.form-actions button {
width: 100%;
margin: 5px 0;
}
}
Adding Theme Customization
Let's implement a theme system with light and dark modes:
javascript
// ClientApp/src/contexts/ThemeContext.js
import React, { createContext, useContext, useState, useEffect } from 'react';
const THEMES = {
LIGHT: 'light',
DARK: 'dark'
};
const ThemeContext = createContext();
export function ThemeProvider({ children }) {
// Try to get theme from localStorage, default to light
const [theme, setTheme] = useState(() => {
const savedTheme = localStorage.getItem('theme');
return savedTheme || THEMES.LIGHT;
});
// Update localStorage when theme changes
useEffect(() => {
localStorage.setItem('theme', theme);
// Apply theme to the document element
document.documentElement.setAttribute('data-theme', theme);
}, [theme]);
const toggleTheme = () => {
setTheme(prevTheme =>
prevTheme === THEMES.LIGHT ? THEMES.DARK : THEMES.LIGHT
);
};
return (
<ThemeContext.Provider value={{ theme, toggleTheme, THEMES }}>
{children}
</ThemeContext.Provider>
);
}
export function useTheme() {
const context = useContext(ThemeContext);
if (context === undefined) {
throw new Error('useTheme must be used within a ThemeProvider');
}
return context;
}
Add a theme-toggle component:
javascript
// ClientApp/src/components/common/ThemeToggle.js
import React from 'react';
import { useTheme } from '../../contexts/ThemeContext';
import './ThemeToggle.css';
export function ThemeToggle() {
const { theme, toggleTheme, THEMES } = useTheme();
return (
<button
className="theme-toggle"
onClick={toggleTheme}
aria-label={`Switch to ${theme === THEMES.LIGHT ? 'dark' : 'light'} mode`}
>
{theme === THEMES.LIGHT ? '🌙' : '☀️'}
</button>
);
}
css
/* ClientApp/src/components/common/ThemeToggle.css */
.theme-toggle {
background: none;
border: none;
font-size: 1.5rem;
cursor: pointer;
padding: 5px;
border-radius: 50%;
transition: background-color 0.2s;
}
.theme-toggle:hover {
background-color: rgba(0, 0, 0, 0.1);
}
Add theme CSS variables to your global CSS:
css
/* ClientApp/src/custom.css (prepend this) */
:root {
/* Light theme (default) */
--background-color: #ffffff;
--text-color: #333333;
--card-bg: #ffffff;
--card-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
--border-color: #dddddd;
--primary-color: #0078d4;
--primary-hover: #005a9e;
--error-color: #e53935;
--success-color: #4caf50;
}
/* Dark theme */
[data-theme='dark'] {
--background-color: #121212;
--text-color: #e0e0e0;
--card-bg: #1e1e1e;
--card-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
--border-color: #444444;
--primary-color: #2196f3;
--primary-hover: #64b5f6;
--error-color: #f44336;
--success-color: #66bb6a;
}
body {
background-color: var(--background-color);
color: var(--text-color);
transition: background-color 0.3s, color 0.3s;
}
/* Update existing components to use CSS variables */
.navbar {
background-color: var(--card-bg);
box-shadow: var(--card-shadow);
}
.auth-card,
.product-card,
.product-detail-card {
background-color: var(--card-bg);
box-shadow: var(--card-shadow);
border: 1px solid var(--border-color);
}
/* Add more component styles using variables... */
Update the App.js to include all providers:
javascript
import React from 'react';
import { Route, Routes, Navigate } from 'react-router-dom';
import { Layout } from './components/Layout';
import { Home } from './components/Home';
import { ProductList } from './components/products/ProductList';
import { ProductDetail } from './components/products/ProductDetail';
import { ProductForm } from './components/products/ProductForm';
import { Login } from './components/auth/Login';
import { Register } from './components/auth/Register';
import { Notifications } from './components/common/Notifications';
import { ProductsProvider } from './contexts/ProductsContext';
import { AuthProvider, useAuth } from './contexts/AuthContext';
import { NotificationProvider } from './contexts/NotificationContext';
import { ThemeProvider } from './contexts/ThemeContext';
import './custom.css';
// Protected route component
function ProtectedRoute({ children }) {
const { isAuthenticated } = useAuth();
if (!isAuthenticated) {
return <Navigate to="/login" />;
}
return children;
}
export default function App() {
return (
<ThemeProvider>
<AuthProvider>
<NotificationProvider>
<ProductsProvider>
<Layout>
<Notifications />
<Routes>
<Route path="/" element={<Home />} />
<Route path="/login" element={<Login />} />
<Route path="/register" element={<Register />} />
<Route
path="/products"
element={
<ProtectedRoute>
<ProductList />
</ProtectedRoute>
}
/>
<Route
path="/product/:id"
element={
<ProtectedRoute>
<ProductDetail />
</ProtectedRoute>
}
/>
<Route
path="/products/new"
element={
<ProtectedRoute>
<ProductForm />
</ProtectedRoute>
}
/>
</Routes>
</Layout>
</ProductsProvider>
</NotificationProvider>
</AuthProvider>
</ThemeProvider>
);
}
Optimizing Performance
React applications can sometimes suffer from performance issues as they grow. Let's implement some optimizations:
React.memo and useCallback
Use React.memo to prevent unnecessary re-renders of components:
javascript
// Example with ProductCard component
import React, { memo } from 'react';
function ProductCard({ product, onSelect }) {
// Component implementation
}
// Export memoized version of the component
export default memo(ProductCard);
Use useCallback to memoize functions:
javascript
import React, { useCallback } from 'react';
export function ProductList() {
// Using useCallback to memoize the handler
const handleProductSelect = useCallback((id) => {
// Implementation
}, [/* dependencies */]);
return (
// Component JSX
);
}
Code Splitting with React.lazy and Suspense
Implement code splitting to reduce the initial bundle size:
javascript
import React, { Suspense, lazy } from 'react';
import { BrowserRouter, Routes, Route } from 'react-router-dom';
import { LoadingSpinner } from './components/common/LoadingSpinner';
// Lazily load components
const Home = lazy(() => import('./components/Home'));
const ProductList = lazy(() => import('./components/products/ProductList'));
const ProductDetail = lazy(() => import('./components/products/ProductDetail'));
const ProductForm = lazy(() => import('./components/products/ProductForm'));
const Login = lazy(() => import('./components/auth/Login'));
const Register = lazy(() => import('./components/auth/Register'));
function App() {
return (
<BrowserRouter>
<Suspense fallback={<LoadingSpinner overlay size="large" />}>
<Routes>
{/* Routes configuration */}
</Routes>
</Suspense>
</BrowserRouter>
);
}
Virtual Scrolling for Long Lists
For applications that display a lot of data, virtual scrolling can significantly improve performance:
javascript
// ClientApp/src/components/products/VirtualizedProductList.js
import React from 'react';
import { FixedSizeList as List } from 'react-window';
import AutoSizer from 'react-virtualized-auto-sizer';
import { useProducts } from '../../contexts/ProductsContext';
import './VirtualizedProductList.css';
// A row renderer component
const Row = ({ index, style, data }) => {
const product = data[index];
return (
<div style={style} className="virtualized-product-row">
<div className="product-card">
<h2>{product.name}</h2>
<p>{product.description}</p>
<p className="price">${product.price.toFixed(2)}</p>
<p className="stock">In stock: {product.stockQuantity}</p>
<a href={`/product/${product.id}`} className="view-button">
View Details
</a>
</div>
</div>
);
};
export function VirtualizedProductList() {
const { products, loading, error } = useProducts();
if (loading) return <p>Loading...</p>;
if (error) return <p className="error-message">{error}</p>;
return (
<div className="virtualized-list-container">
<h1>Product Catalog (Virtualized)</h1>
<div className="virtualized-list">
<AutoSizer>
{({ height, width }) => (
<List
height={height}
width={width}
itemCount={products.length}
itemSize={150} // Adjust based on your row height
itemData={products}
>
{Row}
</List>
)}
</AutoSizer>
</div>
</div>
);
}
Deploying Your Application
Let's cover how to deploy your ASP.NET Core and React application to production.
1. Preparing for Production
Before deploying, you should optimize your application for production:
bash
# Navigate to ClientApp directory
cd ClientApp
# Build React for production
npm run build
The ASP.NET Core template is already configured to serve the static files from the React build output.
2. Azure App Service Deployment
Azure App Service is a common choice for hosting ASP.NET Core applications:
Create a new App Service in the Azure Portal
Set up continuous deployment from your Git repository (GitHub, Azure DevOps, etc.)
Configure environment variables for production settings
Set up a SQL database if needed
You can also deploy using the Azure CLI:
bash
# Login to Azure
az login
# Create a resource group
az group create --name MyReactAppResourceGroup --location eastus
# Create an App Service plan
az appservice plan create --name MyReactAppPlan --resource-group MyReactAppResourceGroup --sku B1
# Create a web app
az webapp create --name MyReactApp --resource-group MyReactAppResourceGroup --plan MyReactAppPlan
# Deploy from a local Git repository
az webapp deployment source config-local-git --name MyReactApp --resource-group MyReactAppResourceGroup
3. Docker Deployment
You can also containerize your application with Docker:
Create a Dockerfile in your project root:
dockerfile
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
WORKDIR /app
# Copy project files
COPY *.csproj ./
RUN dotnet restore
# Copy all files
COPY . ./
# Install Node.js
RUN apt-get update && \
apt-get install -y curl && \
curl -sL https://deb.nodesource.com/setup_18.x | bash - && \
apt-get install -y nodejs
# Build and publish
RUN dotnet publish -c Release -o out
# Build runtime image
FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS runtime
WORKDIR /app
COPY --from=build /app/out ./
# Expose port
EXPOSE 80
EXPOSE 443
ENTRYPOINT ["dotnet", "MyReactApp.dll"]
Build and run the Docker image:
bash
# Build the Docker image
docker build -t myreactapp .
# Run the container
docker run -p 8080:80 myreactapp
Best Practices for ASP.NET Core and React Development
Let's review some best practices for developing with ASP.NET Core and React:
1. Organize Code Effectively
Group related components together
Use feature folders rather than type folders
Keep components small and focused
Separate business logic from UI components
2. Security Best Practices
Implement proper CSRF protection
Use HTTPS in production
Validate all inputs on both client and server
Implement proper authentication and authorization
Use security headers (can be added in ASP.NET Core middleware)
3. Error Handling
Implement global error boundaries in React
Use a centralized error handling middleware in ASP.NET Core
Log errors appropriately
Provide user-friendly error messages
4. Testing
Write unit tests for React components using Jest and React Testing Library
Write unit tests for ASP.NET Core controllers and services
Implement integration tests for API endpoints
Consider end-to-end testing with tools like Cypress
5. Performance Optimization
Use lazy loading for routes and components
Implement server-side rendering for improved SEO and initial load performance
Optimize bundle size with code splitting
Use caching where appropriate
Conclusion
Building dynamic and interactive frontends with ASP.NET Core and React provides a powerful combination of backend stability and frontend flexibility. This pairing leverages Microsoft's enterprise-grade backend framework with Facebook's innovative UI library, enabling developers to create modern web applications that provide exceptional user experiences.
Throughout this article, we've covered:
Setting up your development environment with ASP.NET Core and React
Creating API endpoints in ASP.NET Core
Building React components to consume these APIs
Implementing authentication and authorization
Adding advanced features like form validation and state management
Implementing responsive design and theme customization
Optimizing performance with techniques like code splitting and virtual scrolling
Deploying your application to production environments
By following these patterns and best practices, you'll be well-equipped to build robust, scalable, and maintainable web applications that provide a great experience for your users. The ASP.NET Core and React combination offers the best of both worlds—powerful server-side capabilities and rich client-side interactivity.
As web development continues to evolve, this stack provides a solid foundation that can adapt to changing requirements and emerging technologies. Whether you're building a simple CRUD application or a complex enterprise system, ASP.NET Core and React give you the tools you need to succeed.
Join The Community
Enjoyed this article? There's plenty more where that came from! Subscribe to the ASP Today Substack to get the latest articles, tutorials, and insights delivered straight to your inbox. You'll join a community of passionate ASP.NET developers sharing knowledge and experiences.
Head over to our Substack Chat to connect with fellow developers, ask questions, and share your own projects and successes. Don't miss out on the collaborative learning environment we're building together!