How to Persist a Logged-in User in React
Persisting user login is an important piece of many web applications. You want to keep users logged in even if they refresh the page or close and reopen the browser. Local browser storage gives us the ability to "remember" a logged in user across sessions.
In this comprehensive guide, we‘ll learn how to leverage React to persist a logged-in user with localStorage, step-by-step.
Why User Session Persistence Matters
Before diving into the implementation, let‘s discuss why persisting user sessions provides a better experience.
Logging in can be a frustrating flow for users. So once logged in, we want to avoid forcing users to re-authenticate every time they revisit our app. Some key benefits of persistent login are:
- Convenience: Users can easily pick back up where they left off without re-establishing credentials on every visit
- Security: Sessions expire after periods of inactivity, requiring reauth instead of leaving accounts perpetually logged in
- Caching: Logged-in state allows prefetching and caching personalized app data to speed up loads
By saving a browser session, we both improve convenience through "remembered" login state, while still maintaining security by enforcing eventual re-authentication.
Available Browser Storage Options
Before we jump into implementing session persistence, let‘s explore the browser storage options at our disposal:
- cookies: Key-value data stored in the browser and sent in HTTP headers
- localStorage: Per origin object store for simple data that persists reloads and sessions
- sessionStorage: Per origin object store that clears when tab/browser closes
- IndexedDB: Complex database storage system allowing structured object stores
For most applications, localStorage provides the right balance of simplicity and persistence for user sessions. Cookies can work too but require extra HTTP handling. sessionStorage clears too frequently for persistent logins. And IndexedDB introduces unnecessary overhead for this use case.
So localStorage will be our browser storage mechanism of choice here.
Overview of Local Storage
Let‘s recap how localStorage in particular enables persistent client-side data:
- Data is saved in browser memory, not remote server
- Persists across page refreshes and browser sessions
- Sandboxed by origin (domain + protocol + port)
- Storage limit ~5MB but varies across browsers
- Stored as strings only – must JSON encode objects
This makes localStorage a great fit for persisting user state in the browser! Next let‘s see it in action.
Initial Setup
Let‘s demonstrate persisting user login by setting up a simple React app with form login flow.
First, our App component manages login state with React hooks:
function App() {
const [user, setUser] = useState(null);
const login = (userData) => {
setUser(userData);
}
const logout = () => {
setUser(null);
}
return (
{user ? (
<Dashboard user={user}>
) : (
<LoginForm login={login} />
)}
)
}
We track the logged in user via the user
state. This component will show either the <Dashboard>
or <LoginForm>
depending on if there is a current user.
Our <LoginForm>
then handles authenticating and calling the login
callback on success:
function LoginForm({ login }) {
const handleSubmit = async (credentials) => {
const user = await loginAPI(credentials);
login(user);
}
return (
<form onSubmit={handleSubmit}>
<input name="username"/>
<input name="password" type="password"/>
<button type="submit">Login</button>
</form>
)
}
This covers the core login flow. Next we need to actually persist the user somehow after logging in…
Persisting User State
When the user authenticates in our app, we want to persist their logged in state across page sessions.
This presents two separate challenges:
- How to preserve state on page refresh
- How to preserve state when browser closed or tab closed
To solve these, we leverage:
- The
useEffect
hook to rehydrate state from storage on mount localStorage
to persist user data across sessions
Let‘s see how this works in practice:
function App() {
const [user, setUser] = useState(() => {
// Read from localStorage
return JSON.parse(window.localStorage.getItem("user"));
});
useEffect(() => {
// Persist to localStorage
window.localStorage.setItem("user", JSON.stringify(user))
}, [user]);
return ( /* ... */ )
}
Here‘s what‘s happening on a high level:
On first render, we initialize state from localStorage. This lets us check if the user was already logged in from a previous visit.
We also persist any user state changes back to localStorage via a useEffect
. This handles both the initial login, as well as any later state updates.
And there we have it – user state persisted across page refreshes and browser sessions!
Next let‘s explore this pattern more.
useEffect Explained
React‘s useEffect
hook enables us to replicate lifecycle methods in function components. We can leverage it to handle synchronization logic when state changes.
Here‘s a closer look at our persistence effect:
useEffect(() => {
localStorage.setItem("user", JSON.stringify(user));
}, [user]);
- Executes after render when
user
state changes - JSON stringifies latest
user
value - Writes to
localStorage
to persist data - Effectrerun on any
user
change to keep updated
This makes useEffect
perfect for persisting the state changes that occur during login.
By supplying [user]
as the watch parameter, we rerun the effect whenever that piece of state changes.
There are also other options like useRef
and useCallback
hooks to further optimize performance. But useEffect keeps things simple in most cases.
Initializing State from Storage
We‘ve covered persisting state changes out to storage. Now let‘s initialize state on load:
const [user, setUser] = useState(() => {
const stored = localStorage.getItem("user");
return stored ? JSON.parse(stored) : null;
});
Here we pass a function to useState
that checks localStorage
for any existing user
. If a user exists, we parse and return it as the initial state value.
This allows restoring the user state from a previous session automatically on load.
Security Considerations
While localStorage provides excellent browser persistence, we must keep security in mind:
- localStorage data is accessible via JS on frontend
- Sensitive data could be exposed in XSS attacks
- User data gets sent in headers on each request
Some ways to mitigate risks:
- Store JWTs instead of full user data
- Encrypt values if storing sensitive info
- Send authorization headers instead of cookies
Production apps require thorough security analysis when persisting user data. Additional state protection via libraries like Redux may help limit risk surface area as well.
Bonus: Caching & Prefetching
Persistent user state opens up some exciting possibilities beyond just remembered logins:
- Store user settings in localStorage for customization
- Cache API responses when logged in for speed
- Prefetch personalized dashboard data on revisits
- Prefill forms with user data where applicable
Essentially we can treat the client storage as an extension of the remote server state, while keeping the user experience snappy.
Here are some examples of optimizing app data loading leveraging the persisted user context:
// Prefetch feeds
useEffect(() => {
if (user) {
cache.prefetchFeeds(user.id)
}
}, [user]);
// Auto-complete forms
function SettingsForm() {
const [name, setName] = useState(() => {
const user = localStorage.getItem("user");
return user?.name ?? "";
});
return (
<form>
<input
value={name}
onChange={(e) => setName(e.target.value)}
/>
</form>
)
}
These patterns boost speed for returning users while still maintaining a smooth first-time experience.
Abstracting Storage Logic
As our application grows in scale, we may want to abstract some of the storage persistence logic out into reusable utilities. Here are some examples using custom hooks and higher order components.
Custom Hook
We can create a custom useLocalStorage
hook accepting a key:
function useLocalStorage(key) {
const [state, setState] = useState(() => {
// Read from LS
const storedState = localStorage.getItem(key);
return storedState ? JSON.parse(storedState) : initialState;
});
useEffect(() => {
// Write to LS
localStorage.setItem(key, JSON.stringify(state))
}, [state]);
return [state, setState];
}
This handles reading, parsing, writing and stringifying – keeping components clean:
function App() {
const [user, setUser] = useLocalStorage("user");
// ...
}
Higher Order Component
We could also create a HOC to handle user persistence:
function withUser(Component) {
return (props) => {
const [user, setUser] = useLocalStorage("user");
return (
<Component
{...props}
user={user}
login={(userData) => setUser(userData)}
logout={() => setUser(null)}
/>
);
}
}
Wrapping components enables persisting user state outside core app code:
function App() {
return <Dashboard />
}
export default withUser(Dashboard);
There are many avenues for better organizing persistent state logic – explore what makes sense for your architecture!
Conclusion
And there you have it – everything you need to know to persist user sessions in React apps!
To summarize:
- Why: Session persistence improves convenience and caching potential
- What: Leverage local browser storage like localStorage to store user data
- How: useState and useEffect hooks enable reading/writing storage and syncing state
- More: Security, performance, abstraction etc.
Persistent login has many advantages beyond just remembered credentials. Mastering these concepts opens the door to optimizing data loading, reducing requests, prefilled forms and overall snappier apps!
There are many possible augmentations in complex apps – external state with Redux or React Query, refresh routines for updated tokens, isolated security contexts etc.
But the techniques explored here lay the foundation for real-world user flows. Understanding these core concepts enables building all sorts of authenticated experiences on top.
So get out there, save some user state, and craft incredible persistent applications!