L9: Handle Form Submission
Implement sign-in logic with API calls
Let's implement the actual sign-in logic! 🚀
What Happens When User Signs In?
Sign-in flow:
User submits form
↓
Validate data (Zod)
↓
Send to API (/auth/login)
↓
Receive response
↓
Success: Get tokens + user data
↓
Store in AuthContext
↓
Redirect to dashboardCreate API Client
First, set up Axios for API calls:
npm install axiosCreate API configuration:
import axios from 'axios';
const api = axios.create({
baseURL: 'https://v2.api.noroff.dev',
headers: {
'Content-Type': 'application/json',
},
withCredentials: true, // Important for cookies
});
export default api;Implement signIn in AuthContext
Add the sign-in function to AuthProvider:
import { createContext, useContext, useState, useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import api from '@/lib/api';
const AuthContext = createContext(undefined);
export const useAuth = () => {
const context = useContext(AuthContext);
if (context === undefined) {
throw new Error('useAuth must be used within AuthProvider');
}
return context;
};
export const AuthProvider = ({ children }) => {
const [user, setUser] = useState(null);
const [token, setToken] = useState(null);
const [isLoading, setIsLoading] = useState(true);
const navigate = useNavigate();
// Fetch token on mount (from Lesson 3)
useEffect(() => {
const fetchToken = async () => {
try {
const response = await api.get('/auth/refresh');
const { accessToken, user: userData } = response.data;
setToken(accessToken);
setUser(userData);
} catch (error) {
console.log('No valid session');
} finally {
setIsLoading(false);
}
};
fetchToken();
}, []);
// Sign in function (NEW)
const signIn = async (email, password) => {
try {
// Make API call
const response = await api.post('/auth/login', {
email,
password,
});
// Extract data from response
const { accessToken, data } = response.data;
// Update state
setToken(accessToken);
setUser(data);
// Redirect to home
navigate('/');
return { success: true };
} catch (error) {
// Handle errors
const message = error.response?.data?.errors?.[0]?.message
|| 'Invalid email or password';
return { success: false, error: message };
}
};
// Sign out function
const signOut = async () => {
try {
await api.post('/auth/logout');
} catch (error) {
console.error('Logout error:', error);
} finally {
setUser(null);
setToken(null);
navigate('/sign-in');
}
};
const value = { user, token, isLoading, signIn, signOut };
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
};Update SignInForm
Connect the form to the sign-in function:
import { useState } from 'react';
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { signInSchema } from '@/schemas/signInSchema';
import { useAuth } from '@/contexts/AuthContext';
function SignInForm() {
const { signIn } = useAuth(); // ← Get signIn from context
const [apiError, setApiError] = useState(''); // ← API error state
const {
register,
handleSubmit,
formState: { errors, isSubmitting },
} = useForm({
resolver: zodResolver(signInSchema),
});
const onSubmit = async (data) => {
// Clear previous API errors
setApiError('');
// Call signIn from AuthContext
const result = await signIn(data.email, data.password);
// Handle errors
if (!result.success) {
setApiError(result.error);
}
// Success is handled in AuthContext (navigate to /)
};
return (
<form onSubmit={handleSubmit(onSubmit)} className="sign-in-form">
{/* API Error Message */}
{apiError && (
<div className="api-error" role="alert">
<p>{apiError}</p>
</div>
)}
{/* Email Field */}
<div className="form-field">
<label htmlFor="email" className="form-label">
Email
</label>
<input
id="email"
type="email"
{...register('email')}
className={`form-input ${errors.email ? 'form-input-error' : ''}`}
placeholder="you@example.com"
/>
{errors.email && (
<p className="form-error">{errors.email.message}</p>
)}
</div>
{/* Password Field */}
<div className="form-field">
<label htmlFor="password" className="form-label">
Password
</label>
<input
id="password"
type="password"
{...register('password')}
className={`form-input ${errors.password ? 'form-input-error' : ''}`}
placeholder="••••••••"
/>
{errors.password && (
<p className="form-error">{errors.password.message}</p>
)}
</div>
{/* Submit Button */}
<button
type="submit"
disabled={isSubmitting}
className="submit-button"
>
{isSubmitting ? 'Signing in...' : 'Sign In'}
</button>
</form>
);
}
export default SignInForm;Add API Error Styling
Style the API error message:
/* API Error (server-side errors) */
.api-error {
padding: 0.875rem 1rem;
background: #fef2f2;
border: 1px solid #fecaca;
border-radius: 8px;
margin-bottom: 1rem;
}
.api-error p {
color: #dc2626;
font-size: 0.875rem;
font-weight: 500;
margin: 0;
display: flex;
align-items: center;
gap: 0.5rem;
}
.api-error p::before {
content: '❌';
font-size: 1rem;
}Understanding the Code
Breaking down the signIn function:
const signIn = async (email, password) => {
try {
// 1. Make API call
const response = await api.post('/auth/login', {
email,
password,
});
// 2. Extract data
const { accessToken, data } = response.data;
// 3. Update state
setToken(accessToken);
setUser(data);
// 4. Redirect
navigate('/');
// 5. Return success
return { success: true };
} catch (error) {
// 6. Handle errors
return { success: false, error: 'Error message' };
}
};Step-by-step:
1. POST request to /auth/login
Body: { email, password }
2. API responds (if successful):
{
data: {
accessToken: "eyJhbGc...",
data: { id: 123, name: "John", email: "..." }
}
}
3. Store accessToken and user data in state
→ Other components can now access them
4. Navigate to home page
→ User sees authenticated UI
5. Return success indicator
→ Form knows sign-in workedWhy async/await?
// ❌ Without async/await (harder to read)
function signIn(email, password) {
return api.post('/auth/login', { email, password })
.then(response => {
setToken(response.data.accessToken);
setUser(response.data.data);
navigate('/');
return { success: true };
})
.catch(error => {
return { success: false, error: 'Error' };
});
}
// ✅ With async/await (cleaner)
async function signIn(email, password) {
try {
const response = await api.post('/auth/login', { email, password });
setToken(response.data.accessToken);
setUser(response.data.data);
navigate('/');
return { success: true };
} catch (error) {
return { success: false, error: 'Error' };
}
}Two types of errors:
1. Validation errors (client-side):
// Handled by Zod before API call
email: 'bad-email' → "Invalid email"
password: '123' → "Too short"
// API call never happens2. API errors (server-side):
// Handled in catch block
try {
await api.post('/auth/login', { email, password });
} catch (error) {
// Wrong credentials
// Network error
// Server down
// Rate limit exceeded
}Extracting error message:
const message = error.response?.data?.errors?.[0]?.message
|| 'Invalid email or password';What this does:
1. Try: error.response.data.errors[0].message
→ Noroff API error format
2. If any part undefined, use fallback:
→ 'Invalid email or password'API response examples:
// Success response
{
data: {
accessToken: "...",
data: { ... }
}
}
// Error response (401 Unauthorized)
{
errors: [
{ message: "Invalid email or password" }
],
status: "Unauthorized",
statusCode: 401
}
// Error response (Network)
{
message: "Network Error",
name: "Error",
stack: "..."
}Handling different errors:
catch (error) {
// API responded with error
if (error.response) {
const apiMessage = error.response.data.errors?.[0]?.message;
return { success: false, error: apiMessage || 'Server error' };
}
// Request was made but no response
if (error.request) {
return { success: false, error: 'Network error. Please try again.' };
}
// Something else went wrong
return { success: false, error: 'An unexpected error occurred' };
}API response format:
// Successful login
{
data: {
accessToken: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
data: {
name: "John Doe",
email: "john@example.com",
bio: "Developer",
avatar: {
url: "https://...",
alt: "Profile picture"
},
banner: {
url: "https://...",
alt: "Banner image"
}
}
}
}Destructuring response:
const response = await api.post('/auth/login', { email, password });
// Option 1: Destructure from response.data
const { accessToken, data } = response.data;
// accessToken = "eyJ..."
// data = { name: "John", email: "...", ... }
// Option 2: Access directly
const token = response.data.accessToken;
const userData = response.data.data;Why response.data.data?
Axios response structure:
{
data: { ← response.data (what API sent)
accessToken: "...",
data: { ... } ← response.data.data (user object)
},
status: 200,
statusText: "OK",
headers: { ... },
config: { ... }
}Nested data is API convention:
// API returns:
{
data: { ← Envelope
data: { ... } ← Actual data
}
}
// Some APIs use different names:
{
data: {
user: { ... }
}
}
// Adjust destructuring accordingly:
const { accessToken, user } = response.data;State update order matters:
// ✅ Correct order
setToken(accessToken); // 1. Set token first
setUser(userData); // 2. Set user second
navigate('/'); // 3. Navigate last
// ❌ Wrong order
navigate('/'); // Navigate before state updates
setToken(accessToken); // Token not available yet
setUser(userData); // User not available yetWhy order matters:
Correct order:
setToken → State updated
setUser → State updated
navigate → New page sees updated state ✅
Wrong order:
navigate → New page loads
setToken → State updates (too late)
setUser → State updates (too late)
New page sees old state ❌Batched updates:
React batches state updates in event handlers:
setToken(accessToken); // Queued
setUser(userData); // Queued
// Both update together (one re-render)State updates are asynchronous:
setToken(accessToken);
console.log(token); // ❌ Still old value!
// To use new value, access it directly:
console.log(accessToken); // ✅ New value
// Or use effect:
useEffect(() => {
console.log(token); // Runs after state updates
}, [token]);Updating parent state from child:
// AuthContext (parent)
const signIn = async (email, password) => {
setToken(accessToken); // Updates provider state
setUser(userData);
};
// SignInForm (child)
const onSubmit = async (data) => {
await signIn(data.email, data.password);
// Parent state updated!
};
// Other components (siblings)
function Navbar() {
const { user } = useAuth(); // Gets updated user
// Re-renders automatically when user changes
}Testing Sign-In
Test with valid credentials:
// If you have Noroff API account:
Email: your-email@stud.noroff.no
Password: your-password
// Test account (if provided):
Email: test@test.com
Password: testpassword123- Fill form with valid credentials
- Click "Sign In"
- Should see:
- Button shows "Signing in..."
- Page redirects to
/ - Navbar appears
- No errors
✅ Successful sign-in!
Test with invalid credentials:
- Enter email:
wrong@example.com - Enter password:
wrongpassword - Click "Sign In"
- Should see:
- Red error box appears
- Error message: "Invalid email or password"
- Still on sign-in page
- Form still functional
✅ Error handling working!
Test network error:
- Turn off internet/API
- Fill form
- Click "Sign In"
- Should see error message
✅ Network error handled!
Check React DevTools:
- Open React DevTools
- Find AuthContext.Provider
- Check state:
user: Should have user datatoken: Should have JWT tokenisLoading: Should befalse
✅ State updated correctly!
Test persistence (after refresh):
- Sign in successfully
- Refresh page (F5)
- Should:
- Stay signed in
- Navbar still visible
- User data preserved
✅ Session persists!
(This works because of token refresh in Lesson 3)
Common Issues
What's Next?
In Lesson 10, we'll:
- Test the complete authentication flow
- Add loading states during sign-in
- Handle edge cases
- Improve user experience
✅ Lesson Complete! Sign-in functionality is now fully implemented!
Key Takeaways
- ✅ API client configured with Axios
- ✅ signIn function makes API call and updates state
- ✅ Error handling distinguishes validation vs API errors
- ✅ Response destructuring extracts token and user data
- ✅ State updates happen before navigation
- ✅ Return value indicates success/failure
- ✅ API errors displayed to user
- ✅ CORS handled with
withCredentials - ✅ Try-catch handles all errors gracefully
- ✅ Navigate redirects after successful sign-in