Firebase like reactive authentication store for user state and authentication tokens.
- Store user data and tokens.
- State is synchronized across tabs.
- State can be used in react.
- State can be used outside react, in places like axios interceptors, etc.
- Your own types for user and tokens
- Uses local storage, and is extendable to use other storage options.
yarn add reactive-auth-store
npm install --save reactive-auth-store
store.ts
import {
AuthStore,
LocalStoragePersistance
} from 'reactive-auth-store';
// Define type for user
export interface AuthUser {
id: string
email: string
}
// Define type for tokens
export interface AuthTokens {
access_token: string
refresh_token: string
access_token_expiration: number
refresh_token_expiration: number
}
let store: AuthStore<AuthUser, AuthTokens> | null = null;
const AUTH_STORAGE_KEY = '__auth__'
// function to lazily create store when required
export default function getStore() {
if (store === null) {
store = new AuthStore(
// create persistor to save state
new LocalStoragePersistance(AUTH_STORAGE_KEY),
// a function to compares user equality
(userA, userB) => (userA?.id || null) === (userB?.id || null)
)
}
return store;
}
Login and Logout Users
import getStore from './store';
import type { AuthUser, AuthTokens } from './store';
// Login User
async function login(user : AuthUser, tokens: AuthTokens) {
const store = getStore();
await store.setUser(user);
await store.setTokens(tokens);
}
// Logout
async function logout() {
const store = getStore();
await store.setUser(null);
await store.setTokens(null);
}
Get Current State
const user = getStore().getUser();
const tokens = getStore().getTokens();
Subscribe to Changes
import getStore from './store';
const store = getStore();
store.on('initialized', function() {
console.log('storeInitialized');
});
store.on('userChanged', function(user : AuthUser) {
console.log('userChanged', user);
});
Create a hook to get the current user from the store:
use-auth-user.ts
import getStore from './store';
import type { AuthUser, AuthTokens } from './store';
import { useEffect, useState } from "react"
export const useAuthUser = () => {
const [initialized, setInitialized] = useState(false);
const [user, setUser] = useState<AuthUser | null>(null);
useEffect(() => {
const store = getStore();
const onInitialized = () => {
setInitialized(true);
}
const onUserChanged = (user: AuthUser | null) => {
setUser(user);
}
// initial values
setInitialized(store.getInitialized());
setUser(store.getUser());
// listen to changes
store.on('initialized', onInitialized);
store.on('userChanged', onUserChanged);
return () => {
// cleanup
store.off('initialized', onInitialized);
store.off('userChanged', onUserChanged);
}
}, []);
return { initialized, user }
}
Create a component that allows only logged-in users:
protected.tsx
import { useAuthUser } from './user-auth-user';
import { Navigate } from "react-router-dom";
import * as React from "react";
export function Protected ({ children }: { children: React.ReactElement }) {
const { initialized, user } = useAuthUser();
if (!initialized) {
return null;
}
if (!user) {
return <Navigate to='login' replace={true} />
}
return children;
}
attach-token-interceptor.ts
import { InternalAxiosRequestConfig } from "axios";
import getStore from "./store";
export default function attachTokenInterceptor(
config : InternalAxiosRequestConfig<any>
) {
// attach access token if present
const accessToken = getStore().getTokens()?.accessToken;
if (accessToken) {
config.headers.set('Authorization', `Bearer ${accessToken}`);
}
return config;
}
refresh-token-interceptor.ts
import { InternalAxiosRequestConfig } from 'axios';
import getStore from './store';
export default async function refreshTokenInterceptor(
config : InternalAxiosRequestConfig<any>
) {
// user is not logged
const user = getStore().getUser();
if (!user) {
return config;
}
// refresh access token if expired
await refreshTokenIfExpired();
return config;
}
// refresh access token if it has expired
async function refreshTokenIfExpired() {
// Get current tokens
const store = getStore();
const tokens = store.getTokens();
// Destructure the token properties with default values
const {
access_token='',
refresh_token='',
access_token_expiration=0,
refresh_token_expiration=0,
} = tokens || {};
// check if access token is expired
const now = new Date();
const expiration = new Date(access_token_expiration * 1000);
const tokenExpired = expiration.getTime() <= now.getTime();
// token is still valid
if (!tokenExpired) {
return;
}
// api call to refresh access token
const {
token,
expiration
} = await refreshAccessTokenApi();
// update tokens in store
await store.setTokens({
access_token: token,
access_token_expiration: expiration,
refresh_token: refresh_token,
refresh_token_expiration: refresh_token_expiration,
})
}
Feel free to use and extend the Reactive Auth Store to simplify user authentication and state management in your react application.