use Graft Auth

A credential and whether it is there. The shape is yours, and so is persistence, through defaultValue.

API

Options

proptypedefault
defaultValue?T | nullnullthe token to start from

Returns

proptypedefault
token?T | nullthe credential, in whatever shape yours has
isAuthorized?booleantoken !== null
onAuth?(token: T) => voidstores a token
onAuthRevoke?() => voiddrops it
id?stringstable id

Usage

A token, held

tsx
// -constants/graft_session.ts
export const GraftSession = createGraft({ name: "GraftSession", graft: useGraftAuth<string> });

// -components/session_controls.tsx
export const SessionControls = () => {
  const { isAuthorized, onAuth, onAuthRevoke, token } = GraftSession.use();

  return (
    <>
      <Button onClick={() => (isAuthorized ? onAuthRevoke() : onAuth("jwt-token"))}>
        {isAuthorized ? "sign out" : "sign in"}
      </Button>

      <Chip>{isAuthorized ? `authorized: ${token}` : "anonymous"}</Chip>
    </>
  );
};
previewnothing is written to storage
anonymous

Details

A token, whatever shape yours has

T is open because a token is a different thing on every backend — a bare string here, { access, refresh, expiresAt } there. It is not an invitation to keep something else in here: a user, a set of preferences, anything that is not the credential itself belongs in a store, and this hook would only give it a misleading name.

Because what this hook adds over a store is the vocabulary, not the mechanism. Holding a value is the easy part; being called token, onAuth and isAuthorized is what makes a file recognisable at a glance as the one that decides who you are.

Beyond that it does nothing: isAuthorized is token !== null, nothing is parsed or validated. Refreshing, expiry and roles differ per backend, and a hook that guessed at them would be wrong for most — those go in the hook you write around this one.

Persistence is a decision, not a default

Nothing is written anywhere. No localStorage, no cookie, no interception — which keeps the choice between a cookie, a keychain and nothing at all in your hands, and means the hook renders the same on a server pass as it does in the browser, with nothing to guard and nothing to mismatch on hydration.

The way in is defaultValue, read once on mount: hand it whatever you had stored — reading it is yours to place, and a server pass has no localStorage to read. The way out is an override, since where a token is written is a side of the same decision.

tsx
// -constants/graft_session.ts
export const GraftSession = createGraft({ name: "GraftSession", graft: useGraft });

function useGraft() {
  const auth = useGraftAuth<Session>({ defaultValue: readStoredSession() });

  const onAuth = useCallback((session: Session) => {
    writeStoredSession(session);
    auth.onAuth(session);
  }, [auth]);

  return { ...auth, onAuth };
}