Deep Dive: Handling Session State in React Applications
In modern web applications, managing session state efficiently is critical for ensuring a secure and consistent user experience. In Rica's Plants, I implemented a robust session management solution that leverages the power of React's Context API and browser local storage to persist user authentication across navigations and browser reloads.
Centralizing Session State with React Context
At the core of our session management is a global context created in `src/contexts/SessionContext.js`. This simple context lets our components share the session state without resorting to messy prop drilling. The session context is then provided by the main application in `src/App.jsx`, where we initialize our session token from local storage and expose helper methods for signing in and out.
Consider the following snippet from `src/App.jsx`:
// ... import statements
const App = () => {
// Initialize session token from local storage
const [sessionToken, setSessionToken] = useState(() =>
userService.getSessionTokenStorage()
);
return (
<SessionContext.Provider
value={{
// Decode the JWT token to get the username, or null if no session exists
username: sessionToken ? jwtDecode(sessionToken).username : null,
// Method to sign in: update state and persist token in local storage
signIn: (token) => {
setSessionToken(token);
userService.setSessionTokenStorage(token);
},
// Method to sign out: clear session state and remove token from local storage
signOut: () => {
setSessionToken(null);
userService.removeSessionTokenStorage();
},
}}
>
<BrowserRouter>
<ScrollToTop />
<Routes>
<Route path="/" element={<SignInPage />} />
<Route path="/sign-up" element={<SignUpPage />} />
<Route path="/plants" element={<PlantListPage />} />
<Route path="/plants/:plantId" element={<PlantShowPage />} />
</Routes>
</BrowserRouter>
</SessionContext.Provider>
);
};
export default App;
In this snippet, the session token is retrieved from local storage upon initialization. If a token exists, we extract the username from the JWT using the `jwt-decode` library. Defining `signIn` and `signOut` methods allows our authentication pages to update session state consistently, ensuring that the rest of the application always has access to up-to-date user information.
Persisting Sessions with Local Storage
To allow session persistence even when the user refreshes the page or closes and reopens the browser, we store the session token in local storage. Our service layer encapsulates these operations, keeping the details abstracted from the rest of the application. Below is the relevant code from `src/services/user.js`:
const CAPSTONE_SESSION_TOKEN_KEY = "capstone_session_token";
export const setSessionTokenStorage = (capstoneSessionToken) =>
localStorage.setItem(CAPSTONE_SESSION_TOKEN_KEY, capstoneSessionToken);
export const getSessionTokenStorage = () =>
localStorage.getItem(CAPSTONE_SESSION_TOKEN_KEY);
export const removeSessionTokenStorage = () =>
localStorage.removeItem(CAPSTONE_SESSION_TOKEN_KEY);
By wrapping local storage interactions in our user service, we maintain a clean separation of concerns. Components and contexts simply call these helper functions, unaware of the underlying implementation.
Integrating Session State with API Requests
Beyond user interface concerns, session state is also critical for securing backend interactions. Every API call made by our application includes the session token as part of the request headers. That way, the backend can verify the user's identity and permissions.
In our `src/services/apiFetch.js` utility, we augment all outgoing requests with the session token:
import * as userService from 'services/user';
const { VITE_API_BASE_URL} = import.meta.env;
if (sessionToken) {
const sessionToken = userService.getSessionTokenStorage();
}
const apiFetch = (method, path, body) => {
const options = {
method,
credentials: 'include',
headers: {
authorization: 'Bearer ' + sessionToken || '',
'Content-Type': 'Application/json'
}
};
if (body) {
options.body = JSON.stringify(body);
}
return fetch(VITE_API_BASE_URL + path, options);
};
export default apiFetch;
This design ensures that every API request is automatically authenticated. If no session token is present, the backend can immediately reject unauthorized requests, adding an extra layer of security.
Enforcing Route Protection Using Session State
Another important aspect of session management is controlling access to various routes. For example, components like `RedirectToPlantsIfSignedIn` and `RedirectToSignInIfSignedOut`—located in the shared components—leverage the SessionContext to determine whether a user should be redirected away from or toward a particular page.
By centralizing this logic within our context, we can enforce route protection consistently across the entire application.
Conclusion
The session management solution in Rica's Plants is built on the solid foundations of React's Context API and browser local storage. By combining these technologies, the project ensures that user authentication is persistent, secure, and easily accessible throughout the application. With helper functions abstracting storage details and middleware automatically injecting session information into API requests, this design provides a clear, maintainable approach to session state management—an essential consideration for any modern web application.