Below is a refactor of your code which includes inline comments to explain. Because you are performing asynchronous tasks, you'll need to support cancellation or you'll encounter a common no-op memory leak error, as described in this question: Can't perform a React state update on an unmounted component.
Note: The code in your question includes TypeScript syntax, so I wrote the answer using TypeScript syntax. If you want plain JavaScript for some reason, the linked TypeScript Playground includes the transpiled JSX.
TS Playground
import { type ReactElement, useCallback, useEffect, useRef, useState } from "react";
/** A custom hook for discriminating first render */
function useIsFirstRender (): boolean {
  const ref = useRef(true);
  return ref.current ? !(ref.current = false) : false;
}
// Based on the code you provided:
type User = Record<"email" | "name", string>;
type UserResponse = { auth: User[] };
// This is not a closure, so moving it outside the component
// allows for stable object identity:
async function getUsers (
  { signal = null }: { signal?: AbortSignal | null | undefined } = {},
): Promise<User[]> {
  // Using the AbortSignal API allows for cancellation:
  signal?.throwIfAborted();
  // Alternatively — if your target environment doesn't yet support the above method:
  // if (signal?.aborted) throw signal.reason;
  const response = await fetch("/api/users", { signal });
  const data = await response.json() as UserResponse;
  return data.auth;
};
function Users (): ReactElement {
  const isFirstRender = useIsFirstRender();
  const [users, setUsers] = useState<User[]>([]);
  // Put the state-updating functionality in a reusable closure function.
  // This function needs to be wrapped by the useCallback hook
  // in order to maintain a stable object identity across renders:
  const updateUsers = useCallback(async (
    { signal }: { signal?: AbortSignal } = {},
  ): Promise<void> => {
    try {
      const users = await getUsers({ signal });
      setUsers(users);
    }
    catch (ex) {
      if (signal?.aborted && Object.is(ex, signal.reason)) {
        // Handle aborted case here (probably a no-op in your situation):
        console.error(ex);
      }
      else {
        // Handle other exceptions here:
        console.error(ex);
      }
    }
  }, [setUsers]);
  useEffect(() => {
    const controller = new AbortController();
    const { signal } = controller;
    // Immediately update users on first render only:
    if (isFirstRender) updateUsers({ signal });
    // Set an interval to update users every 30s
    const timerId = setInterval(() => updateUsers({ signal }), 30e3);
    // Cleanup function:
    return () => {
      // Abort any in-progress fetch requests and updates:
      controller.abort(new Error("Component is re-rendering or unmounting"));
      // Cancel the interval:
      clearInterval(timerId);
    };
  }, [isFirstRender, updateUsers]);
  const userListItems = users.length > 0
    ? users.map(user => {
      // Ref: https://reactjs.org/docs/lists-and-keys.html#keys
      // Keys must be both unique and stable:
      const value = `${user.name} - ${user.email}`;
      return (<li key={value}>{value}</li>);
    })
    : null;
  return (
    <div>
      <h2>Users</h2>
      {/* List item elements should be children of list elements */}
      <ul>{ userListItems }</ul>
    </div>
  );
}
export default Users;