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:

  1. How to preserve state on page refresh
  2. How to preserve state when browser closed or tab closed

To solve these, we leverage:

  1. The useEffect hook to rehydrate state from storage on mount
  2. 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!

Similar Posts