@remate/core
v0.0.15
Published
remate is a React-based boilerplate for building internal tools, rapidly.
Downloads
177
Maintainers
Readme
Remate
Remate is a purpose-built React framework designed for developing CRUD-intensive web applications. It is ideal for enterprise-level use cases, including internal tools, admin panels, dashboards, B2B platforms, and other client-side applications.
Remate's core hooks and components streamline the development process by offering industry-standard solutions for crucial aspects of a project, including authentication, access control, routing, networking, and state management.
Overview
Authentication & Authorization
Remate offers a comprehensive solution for secure user management and fine-grained access control:
Authentication: Seamless integration with various authentication methods, including:
- Social logins (e.g., Google, Facebook)
- Email/password authentication
- OAuth and custom providers
Advanced Authorization Mechanisms:
- Role-Based Access Control (RBAC)
- Permission-Based Access Control (PBAC)
- Attribute-Based Access Control (ABAC)
- Claim-Based Access Control (CBAC)
Encrypted Session Handling: Ensures secure storage of sensitive tokens and user data with encrypted local storage.
Data Fetching (asynchronous state management)
Built around react-query, the Data Fetching system ensures efficient communication with backend services. Key features include:
Query Handling:
- Error Management: Centralized error tracking and user-friendly feedback mechanisms.
- Loading State Management: Intuitive loading indicators with cancellation support for redundant or outdated requests.
- Infinite Query Support: Handles paginated data fetching with ease, enabling smooth scrolling experiences.
Mutation Handling:
- Optimistic Updates: Allows seamless user experiences by updating UI state before server confirmation.
- Automatic Cache Invalidation: Ensures consistent data by refreshing dependent queries after mutations.
Routing and Navigation
- Dynamic Navigation State: Facilitates the creation of breadcrumbs, page titles, and document titles based on route configuration.
- Layout Configuration: Customizable layouts for individual routes to enhance modularity and reusability.
- Secure Routing: Integrates access control directly into routing logic to secure sensitive routes.
Utilities
- Reusable Helper Functions: Provides a library of utility functions to address common development tasks, such as formatting, data validation, and deep comparisons.
- Custom React Hooks: Includes hooks for state management, side-effect handling, and component lifecycle needs.
Architecture
- Headless Design: Adopts a UI-agnostic architecture to allow seamless integration with any UI framework or design system.
- TypeScript-Driven Development: Ensures type safety and enhanced developer experience.
- Extendable Modules: Offers a modular design, enabling developers to customize and extend functionalities based on project requirements.
Benefits
- Developer Productivity: Streamlined workflows with pre-built modules and utilities.
- Security and Reliability: Adherence to best practices in authentication, storage, and API communication.
- Scalability: Designed to handle growing application demands with ease.
- Flexibility: Supports diverse application requirements through modular and customizable architecture.
Quick Start Guide
The quickest way to start using Remate is through the Remate Boilerplate, which sets up a ready-to-use development environment with minimal effort.
Alternatively, you can manually integrate Remate into your existing project. Below is an example of setting up a basic application with Remate:
Setting Up Remate Manually
Installation
Start by installing the core Remate package and the necessary peer dependencies:
npm install @remate/core react react-dom react-router @tanstack/react-queryCreate Your Application Entry Point:
This is the main entry point for the Remate application, which includes routing, layout management, authentication, and notifications.
import { Remate, RemateRoutesType, IAuthProviderAdapter, NotificationProviderType, ILayoutProps } from '@remate/core';
import { BrowserRouter } from 'react-router';
import { toast } from 'react-toastify';
import DefaultLayout from './layouts/DefaultLayout';
import CustomLayout from './layouts/CustomLayout';
import HomePage from './pages/HomePage';
import SignInPage from './pages/SignInPage';
import internalAuthProvider from './auth/internalAuthProvider';
const layouts = {
// Define your application layouts here
default: ({ config: configFromRouteConfig, children }: ILayoutProps) => (
<DefaultLayout config={configFromRouteConfig}>{children}</DefaultLayout>
),
custom: CustomLayout1
};
const routes: RemateRoutesType = [
// Define your application routes here
{ path: '/', element: <HomePage />, privileges: 'admin', title: 'Home' },
{
path: '/signin',
element: <SignInPage />,
privileges: 'guest',
title: 'sign in',
layout: {
style: 'custom',
config: {
/* Custom config for this layout, e.g., no sidebar, full screen */
}
}
}
];
// Notification provider setup using react-toastify for example
const notificationProvider: NotificationProviderType = {
open: ({ key, message, type }) => {
if (toast.isActive(key as string)) {
toast.update(key as string, {
render: message,
closeButton: true,
autoClose: 5000,
type
});
} else {
toast(message, {
toastId: key,
type
});
}
},
close: (key) => toast.dismiss(key)
};
function App() {
return (
<BrowserRouter>
<Remate
routes={routes}
notificationProvider={notificationProvider}
layouts={layouts}
defaultLayoutStyle="default"
authProvider={internalAuthProvider}
>
{/* children components */}
</Remate>
</BrowserRouter>
);
}
export default App;Table of Contents
Authentication and Authorization
Authentication and authorization are foundational aspects of securing modern web applications. Remate provides a robust and extensible framework to handle these requirements seamlessly.
Authentication
Authentication is the process of verifying the identity of a user or client. It's a critical component of security, ensuring that only authorized users can access certain features or data within the application. Whether you are building a complex enterprise-level application or a simple CRUD interface, Remate's authentication system provides the necessary infrastructure to protect your pages and ensure that users interact with your application in a secure and controlled manner.
Remate's flexible architecture allows you to easily implement various authentication strategies like social logins (e.g., Google, Facebook), email/password authentication and OAuth and custom providers.
Authorization
Authorization is a key aspect of security and user experience in web applications. Whether you are building a complex enterprise-level application or a simple CRUD interface, Remate's authorization system provides the necessary infrastructure to protect your resources and ensure that users interact with your application in a secure and controlled manner.
Remate's flexible architecture allows you to easily implement various authorization strategies:
- Role-Based Access Control (RBAC)
- Permission-based Access Control (PBAC)
- Claim-based Access Control (CBAC)
- Attribute-Based Access Control (ABAC)
With any authorization solution.
Remate offers several features to help you implement authorization in your application:
- Component-Level Control: Use the
<CanAccess />component to render elements conditionally based on access. - Programmatic Checks: The
canAccessfunction (available in theuseAuthhook) enables programmatic access control logic. - Route Security: control route access, ensuring users see only what they are authorized for.
Auth Provider
Remate can use any authentication backend, but you have to write an adapter for it. This adapter is called an authProvider. The authProvider is a simple object with methods that Remate calls to handle authentication and authorization.
IAuthProviderAdapter Interface Overview
The authProvider is defined by the IAuthProviderAdapter interface, which specifies the following methods:
interface IAuthProviderAdapter<TUser, TUserPrivileges, TRequestedPrivileges> {
signInPageUrl: string;
loginRedirectUrl: string;
onError: (error: any) => Promise<OnErrorResponse> | OnErrorResponse | null | Promise<null>;
initialCheck: () => Promise<CheckResponse<TUser>> | CheckResponse<TUser>;
getUser: () => Promise<TUser> | TUser;
signOut: (...args: any[]) => Promise<SignOutResponse> | SignOutResponse;
signIn: (...args: any[]) => Promise<AuthActionResponse<TUser>> | AuthActionResponse<TUser>;
initialUserState?: TUser | null;
getUserPrivileges?: (user: TUser) => Promise<TUserPrivileges> | TUserPrivileges;
canAccess?: (requestedPrivileges: TRequestedPrivileges, userPrivileges: TUserPrivileges) => boolean;
signUp?: (...args: any[]) => Promise<AuthActionResponse<TUser>> | AuthActionResponse<TUser>;
updateUser?: (userData: PartialDeep<TUser>, currentUser: TUser) => Promise<TUser> | TUser;
checkingAuthFallback?: ReactNode;
routeAccessDeniedFallback?: ReactNode;
}Example Implementation
Here is a fictive but working implementation of an auth provider. It only accepts user “john” with password “123”.
import { IAuthProviderAdapter } from '@remate/core';
type Privileges = 'user' | 'guest';
interface IUser {
id: string;
fullName: string;
role: Privileges;
}
interface ISignInCredentials {
username: string;
password: string;
}
const authProvider: IAuthProviderAdapter<IUser, Privileges> = {
signInPageUrl: '/sign-in',
loginRedirectUrl: '/',
async signIn({ username, password }: ISignInCredentials) {
if (username !== 'john' || password !== '123') {
return {
success: false,
error: new Error('Login failed')
};
}
localStorage.setItem('username', username);
return {
success: true,
user: { id: username, fullName: username, role: 'user' }
};
},
async signOut() {
localStorage.removeItem('username');
return { success: true };
},
async initialCheck() {
const authenticated = localStorage.getItem('username') !== null;
return { authenticated };
},
async getUser() {
const username = localStorage.getItem('username');
return { id: username, fullName: username, role: 'user' };
},
async getUserPrivileges(user) {
return user.role;
}
};
export default authProvider;using authProvider
The following example demonstrates how to using authProvider and pass them to the Remate component:
import { Remate, ILayoutProps } from '@remate/core';
import { BrowserRouter } from 'react-router';
import authProvider from './auth/authProvider';
function App() {
return (
<BrowserRouter>
<Remate authProvider={authProvider}>{/* Children components */}</Remate>
</BrowserRouter>
);
}
export default App;Step-By-Step Guide for Auth Provider Implementation
signInPageUrl
The pathname to redirect users when they need to log in.
loginRedirectUrl
The default pathname to redirect users after a successful login.
onError
this method is called when you get an error response from the API. You can create your own business logic to handle the error such as refreshing the token, logging out the user, etc.
Example Implementing onError
Below is a basic example of how to define and use the onError method:
Auth Provider Implementation
import React from 'react';
import { IAuthProviderAdapter } from '@remate/core';
export const authProvider: IAuthProviderAdapter = {
onError(error) {
if (error.response?.status === 401) {
return { logout: true };
}
return null;
}
// Other methods like signOut, etc., would go here.
};signIn
The signIn method is responsible for handling the logic of logging users into the application with the current authentication method. You will typically call this method using the signIn function provided by the useAuth hook.
When defining the signIn method within the authProvider, it should return a object based on SignInResponse interface. This response may include user data. If the user field is provided in the response, the getUser method will not be called after login.
Example: Implementing signIn
Below is a basic example of how to define and use the signIn method:
Auth Provider Implementation
import React from 'react';
import { IAuthProviderAdapter, SecureLocalStorage } from '@remate/core';
export interface IUser {
email: string;
}
export interface ISignInCredentials {
email: string;
password: string;
}
export const authProvider: IAuthProviderAdapter<IUser> = {
async signIn({ email, password }: ISignInCredentials) {
// Simplified example: Instead of sending a request, we're saving the credentials in localStorage.
// In production, send a secure request to the server and store tokens securely.
SecureLocalStorage.setItem('token', { email, password });
alert('You are logged in!');
return {
success: true,
user: { email }
};
}
// Other methods like signOut, check, etc., would go here.
};Login Page Implementation
Here’s an example of a basic login page that utilizes the signIn method from the useAuth hook:
import React from 'react';
import { useAuth } from '@remate/core';
export const LoginPage = () => {
const { signIn } = useAuth();
const onSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
// Extract form data
const formData = Object.fromEntries(new FormData(e.currentTarget).entries());
// Call the signIn method
signIn(formData);
// Reset the form
e.currentTarget.reset();
};
return (
<div>
<h1>Sign In</h1>
<form onSubmit={(e) => onSubmit(e)}>
<input
type="email"
name="email"
placeholder="Email"
required
/>
<input
type="password"
name="password"
placeholder="Password"
required
/>
<button type="submit">Submit</button>
</form>
</div>
);
};signUp
The signUp method is an optional function designed to handle the logic for registering new users in the application using the current authentication method. You can typically call this method via the signUp function provided by the useAuth hook.
When implementing the signUp method within the authProvider, it should return an object conforming to the SignInResponse interface. This response may include user data, and if the user field is provided, the getUser method will not be called after the user is logged in.
Note: After calling
signUp(provided byuseAuth), the user is authenticated and logged into the application in the same way as withsignIn.
Example: Implementing signUp
Below is a simple example of how to define and use the signUp method:
Auth Provider Implementation
import React from 'react';
import { IAuthProviderAdapter, SecureLocalStorage } from '@remate/core';
export interface IUser {
email: string;
}
export interface ISignUpCredentials {
email: string;
password: string;
}
export const authProvider: IAuthProviderAdapter<IUser> = {
async signUp({ email, password }: ISignUpCredentials) {
// Simplified example: Instead of sending a request, credentials are saved directly in localStorage.
// In a real-world application, send a secure request to the server and store tokens securely.
SecureLocalStorage.setItem('token', { email, password });
alert('You are now registered and logged in!');
return {
success: true,
user: { email }
};
}
// Additional methods like signIn, signOut, and check would go here.
};Register Page Implementation
Here’s an example of a basic registration page that utilizes the signUp method from the useAuth hook:
import React from 'react';
import { useAuth } from '@remate/core';
export const RegisterPage = () => {
const { signUp } = useAuth();
const onSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
// Extract form data
const formData = Object.fromEntries(new FormData(e.currentTarget).entries());
// Call the signUp method
signUp(formData);
// Reset the form
e.currentTarget.reset();
};
return (
<div>
<h1>Register</h1>
<form onSubmit={onSubmit}>
<input
type="email"
name="email"
placeholder="Email"
required
/>
<input
type="password"
name="password"
placeholder="Password"
required
/>
<button type="submit">Submit</button>
</form>
</div>
);
};signOut
The signOut method is used to define the logout logic for the application with the current authentication strategy. You can call this method using the signOut function provided by the useAuth hook.
When implementing the signOut method within the authProvider, it should return an object conforming to the SignOutResponse interface. If the logout process completes successfully and you want the user to immediately log out, set the success property to true in the returned object. Alternatively, if you want to implement a redirect logic during logout, use the redirect property and provide the desired URL.
Additionally, you can define and pass any parameters needed for your signOut method.
Example: Implementing signOut
Below is a simple example of how to define and use the signOut method:
Auth Provider Implementation
import React from 'react';
import { IAuthProviderAdapter, SecureLocalStorage } from '@remate/core';
export interface IUser {
email: string;
}
export const authProvider: IAuthProviderAdapter<IUser> = {
async signOut() {
SecureLocalStorage.removeItem('token');
alert('You are now logged out!');
return {
logout: true // Log the user out immediately
};
}
// Additional methods like signIn, signUp, and check would go here.
};Using the signOut Method
Here’s an example of how to use the signOut method from the useAuth hook within a component:
import React from 'react';
import { useAuth } from '@remate/core';
import { IUser } from '../authProvider';
export const AppToolbar = () => {
const { signOut, user } = useAuth<IUser>();
return (
<div>
<p>User Email: {user.email}</p>
<button onClick={() => signOut()}>Sign Out</button>
</div>
);
};initialCheck
The initialCheck method is a critical part of the authentication process, ensuring that the user's session remains valid after login or registration. This method verifies if the user is currently authenticated and provides the necessary data to maintain their session. It works in conjunction with the isAuthenticated property provided by the useAuth hook to determine the user's authentication status.
this method is used to check if the user is authenticated. It is internally called as an initializer. It is expected to return a resolved promise conforming to the CheckResponse type. The response should include:
authenticated: A boolean indicating the user's authentication status.user(optional): User data. If this is provided, thegetUsermethod will not be invoked after the check.
Example: Implementing the initialCheck Method
Below is an example of how to define the initialCheck method in the authProvider:
Auth Provider Implementation
import React from 'react';
import { IAuthProviderAdapter, SecureLocalStorage } from '@remate/core';
export interface IUser {
email: string;
}
export const authProvider: IAuthProviderAdapter<IUser> = {
async initialCheck() {
const token = SecureLocalStorage.getItem('token');
if (token && token.email) {
return {
authenticated: true,
user: { email: token.email }
};
}
return {
authenticated: false
};
}
// Additional methods like signIn, signUp, and signOut would go here.
};Using the isAuthenticated Property
Here’s an example of how to use the isAuthenticated property from the useAuth hook to manage user authentication status:
Example Component: App Toolbar
import React from 'react';
import { useAuth } from '@remate/core';
import { Link } from 'react-router';
import { IUser } from '../authProvider';
export const AppToolbar = () => {
const { isAuthenticated, user, signOut } = useAuth<IUser>();
return (
<div>
{isAuthenticated ? (
<>
<p>User Email: {user.email}</p>
<button onClick={() => signOut()}>Sign Out</button>
</>
) : (
<Link to="/sign-in">Sign In</Link>
)}
</div>
);
};getUser
The getUser method is essential for adapting application components to the current user's identity. For instance, a locking system may allow editing only if the current user is the lock owner. Similarly, user menus may display the current user's name and avatar.
This method is expected to return a resolved promise conforming to the TUser type from the IAuthProviderAdapter interface.
The getUser method is automatically invoked when the check, signIn, or signUp methods are executed without returning a user in their responses. Additionally, the refreshUser method can be used to internally re-fetch the user data, ensuring the user state reflects the latest data returned by getUser.
Example: Implementing the getUser Method
Below is an example of how to define the getUser method in the authProvider:
Auth Provider Implementation
import React from 'react';
import { IAuthProviderAdapter, SecureLocalStorage } from '@remate/core';
export interface IUser {
email: string;
}
export const authProvider: IAuthProviderAdapter<IUser> = {
async getUser() {
// Simplified example: Retrieve user data directly from localStorage.
// In a real-world scenario, make a secure server request to fetch user details.
const token = SecureLocalStorage.getItem('token');
if (token && token.email) {
return { email: token.email };
}
throw new Error('User is not authenticated');
}
// Additional methods like signIn, signUp, and signOut would go here.
};Using the user Property and refreshUser method
Here’s an example of how to use the user property and refreshUser method from the useAuth hook to access and refetch user information:
Example Component: App Toolbar
import React from 'react';
import { useAuth } from '@remate/core';
import { IUser } from '../authProvider';
export const AppToolbar = () => {
const { user, refreshUser } = useAuth<IUser>();
return (
<div>
<p>User Email: {user.email}</p>
<button onClick={() => refreshUser()}>Refresh User Data</button>
</div>
);
};getUserPrivileges
The getUserPrivileges method allows you to define and retrieve the access privileges of a user, enabling you to check their permissions for interacting with system resources. The user object is passed as a parameter to this method and you can use it if needed.
This method must return a value based on the TPrivileges type from the IAuthProviderAdapter interface. The output of this method is primarily utilized in the authProvider.canAccess method to validate access to specific resources.
Example: Implementing getUserPrivileges
Below is an example of how to define the getUserPrivileges method within an authProvider:
Auth Provider Implementation
import React from 'react';
import { IAuthProviderAdapter } from '@remate/core';
// Define privilege levels
export type Privileges = 'admin' | 'staff' | 'user';
// Define the user structure
export interface IUser {
role: Privileges;
email: string;
}
export const authProvider: IAuthProviderAdapter<IUser, Privileges> = {
async getUserPrivileges(user) {
// Return the user's role as their privilege
return user.role;
},
// The `canAccess` method is pre-defined by default.
// However, you can customize it as needed.
canAccess(requestedPrivilege, userPrivileges) {
// Example logic: Grant access if requested privilege matches the user's privilege
return requestedPrivilege === userPrivileges;
}
// Additional methods like signIn, signUp, and signOut would go here.
};canAccess
The canAccess method provides a flexible way to define custom logic for managing access to specific resources in your application. This method is versatile and can be used in multiple contexts, such as:
- Component-Level Access Control: Using the
CanAccesscomponent. - Route Guards: Controlling access at the routing level.
- Programmatic Access Checks: Through the
canAccessmethod available in theuseAuthhook.
It supports the implementation of various access control strategies, including RBAC (Role-Based Access Control), PBAC (Permission-Based Access Control), and others.
Parameters
The method takes the following two parameters:
- Requested Privilege: This represents the required privilege level for a resource, passed by a route, the
CanAccesscomponent, or other. - User Privileges: These are the current privileges of the user, obtained from the
authProvider.getUserPrivilegesmethod.
The method returns a boolean value indicating whether the user is authorized to access the requested resource.
Key Notes
- Execution Context: The
canAccessmethod is only invoked when the user is authenticated via the current authentication method. If the user is not authenticated, canAccess check user is authenticated at first. - Public Resources: For resources that do not require authentication or authorization (e.g., a public
Homepage), do not define the requested privilege(auth) or set it tonull.
The default implementation for the canAccess method is as follows. This logic can be customized to meet your application's specific requirements:
function defaultCanAccess(requestedPrivileges: RequestedPrivileges, userPrivileges: unknown) {
/**
* Allow access if no privileges are required.
*/
if (requestedPrivileges === null || requestedPrivileges === undefined) {
return true;
}
/**
* If the required privileges array is empty,
* allow access only if the user role is 'guest' (null or empty array).
*/
if (Array.isArray(requestedPrivileges) && requestedPrivileges.length === 0) {
return !userPrivileges || (Array.isArray(userPrivileges) && userPrivileges.length === 0);
}
/**
* Check for matching privileges between requested and user privileges.
*/
if (userPrivileges && Array.isArray(userPrivileges)) {
if (Array.isArray(requestedPrivileges)) {
return requestedPrivileges.some((privilege) => userPrivileges.includes(privilege));
}
return userPrivileges.includes(requestedPrivileges);
}
if (Array.isArray(requestedPrivileges)) {
return requestedPrivileges.includes(userPrivileges as string);
}
return requestedPrivileges === userPrivileges;
}Example: Customizing the canAccess Logic
Below is an example of how to override the default canAccess logic for specific use cases:
Auth Provider Implementation
import React from 'react';
import { IAuthProviderAdapter } from '@remate/core';
// Define privilege levels
export type Privileges = 'admin' | 'staff' | 'user';
// Define the user structure
export interface IUser {
role: Privileges;
email: string;
}
export const authProvider: IAuthProviderAdapter<IUser, Privileges> = {
async getUserPrivileges(user) {
// Return the user's role as their privilege
return user.role;
},
// Customize the `canAccess` logic
canAccess(requestedPrivilege, userPrivileges) {
/**
* Example: Grant access if the requested privilege
* matches the user's privilege.
*/
return requestedPrivilege === userPrivileges;
}
// Additional methods like signIn, signUp, and signOut would go here.
};updateUser
The updateUser method is an optional but highly useful feature that allows updating user information. For example, you can send a request to the server to update the user's data and return the updated user object. This method must return the updated user data conforming to the TUser type defined in the IAuthProviderAdapter interface.
parameters:
userData: APartialDeep<TUser>object containing the updated user fields.currentUser: The current state of the user object, which you can use if needed to calculate or validate updates.
Typically, this method is invoked using the updateUser function provided by the useAuth hook.
Example: Implementing updateUser
Below is an example of how to define and use the updateUser method in the authProvider:
Auth Provider Implementation
import React from 'react';
import { IAuthProviderAdapter } from '@remate/core';
export interface IUser {
email: string;
firstName: string;
lastName: string;
}
export const authProvider: IAuthProviderAdapter<IUser> = {
async updateUser(userData, currentUser) {
/**
* Simplified example: Merge the new user data with the old user data.
* In a real-world scenario, send a request to the server and store the updated data.
*/
return { ...currentUser, ...userData };
}
// Other methods like signIn, signOut, and check would go here.
};Example: Update Profile Component
The following is an example of a simple "Update Profile" page that uses the updateUser method from the useAuth hook:
Update Profile Page
import React from 'react';
import { useAuth } from '@remate/core';
export const UpdateProfilePage = () => {
const { updateUser } = useAuth();
const onSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
// Extract form data
const formData = Object.fromEntries(new FormData(e.currentTarget).entries());
// Call the updateUser method
updateUser(formData);
// Reset the form
e.currentTarget.reset();
};
return (
<div>
<h1>Update Profile</h1>
<form onSubmit={onSubmit}>
<input
type="text"
name="firstName"
placeholder="First Name"
required
/>
<input
type="text"
name="lastName"
placeholder="Last Name"
required
/>
<button type="submit">Submit</button>
</form>
</div>
);
};checkingAuthFallback
A fallback React node to display while authentication checks are in progress.
Example
import React from 'react';
import { IAuthProviderAdapter } from '@remate/core';
export const authProvider: IAuthProviderAdapter = {
checkingAuthFallback: <h1>chcking auth...</h1>
// Other methods like signIn, signOut, and check would go here.
};routeAccessDeniedFallback
A fallback React node to display while user has not Route requested privileges.
if routeAccessDeniedFallback not provided, user will be redirect to loginRedirectUrl by default if has not access to the route.
Example
import React from 'react';
import { IAuthProviderAdapter } from '@remate/core';
export const authProvider: IAuthProviderAdapter = {
routeAccessDeniedFallback: <h1>Access Denied</h1>
// Other methods like signIn, signOut, and check would go here.
};initialUserState
The initial state of user (before sign in). user state is null by default.
useAuth Hook
The useAuth hook provides all the necessary methods and information for handling authentication and authorization in your application. We've already seen examples of this hook in use; now, let’s delve into its type to better understand its capabilities.
Type Overview
Below is the type Overview for the useAuth hook, which is generic and can be tailored to your specific user (TUser) and privilege (TPrivileges) types:
type useAuth = <TUser = unknown, TUserPrivileges = unknown, TRequestedPrivileges = any>() => {
isAuthenticated: boolean;
user: TUser;
setUser: (value: TUser | ((lastUserState: TUser) => TUser)) => void;
resetUser: () => void;
refreshUser: () => void | Promise<void>;
userPrivileges: TUserPrivileges | null;
signOut: <T extends unknown[]>(...args: T) => void | Promise<void>;
signIn: <T extends unknown[]>(...args: T) => void | Promise<void>;
signUp: <T extends unknown[]>(...args: T) => void | Promise<void>;
updateUser: (userData: PartialDeep<TUser>) => void | Promise<void>;
canAccess: (requestedPrivileges: TRequestedPrivileges) => boolean;
checkError: (error: any) => void;
isProvided: boolean;
};Explanation of Methods and Properties
isAuthenticated:
A boolean value indicating whether the user is currently authenticated.user:
The current user object, which is of typeTUser. This holds all user-specific details.setUser:
A method to update the user object.- Accepts either a new
TUserobject or a function that takes the current user state and returns the updated state.
- Accepts either a new
resetUser:
A method to reset the user state to initial user state.refreshUser:
A method to refresh the current user state by fetching the latest data (call thegetUsermethod and set user state based on that).userPrivileges:
The current privileges assigned to the user, of typeTPrivileges.
If the user is unauthenticated, this will benull.checkError:
this method calls the onError method from the authProvider under the hood.signOut:
Logs the user out. Can be asynchronous if additional cleanup or server communication is required.signIn:
A generic method for signing in the user using additional arguments.signUp:
Similar tosignIn, this method is for registering a new user. Accepts an authentication method and arguments.isProvided:
a boolean value that ditermines authProvider is provided or notupdateUser:
Updates the user's information using a partial object (PartialDeep<TUser>) containing the updated fields.canAccess:
A method to determine if the user has the required privileges to access a resource.- Takes
requestedPrivilegesas input. - Returns a boolean.
- Takes
Example Usage
Here’s a basic example of how to use the useAuth hook in a React component:
import React from 'react';
import { useAuth } from '@remate/core';
export const UserProfile = () => {
const { user, isAuthenticated, signOut, updateUser, canAccess } = useAuth();
const handleSignOut = () => {
signOut();
};
const handleUpdateProfile = () => {
const updatedUserData = { firstName: 'John', lastName: 'Doe' };
updateUser(updatedUserData);
};
if (!isAuthenticated) {
return <div>Please log in to access your profile.</div>;
}
return (
<div>
<h1>Welcome, {user.firstName}</h1>
<button onClick={handleUpdateProfile}>Update Profile</button>
<button onClick={handleSignOut}>Log Out</button>
{canAccess('admin') && <div>You have admin access.</div>}
</div>
);
};Router Integrations
To manage access control for your routes, for check authenticated you can use authenticated option in route configuration and for authorization you can define the requested privilege for each route. This is done by setting the desired privileges property on the route configuration. Behind the scenes, the privileges value is passed to the canAccess method. Based on the result:
- If the user has the required access, the route will be displayed.
- If not, the user will be redirected to the
loginRedirectUrlfrom authProvider by default or displayrouteAccessDeniedFallbackif provided.
Propagation to Child Routes
If a route has the privileges or authenticated property defined, the same privilege level will be applied to all its child routes, unless an privileges or authenticated property is explicitly defined for a specific child route.
Example: Setting Up Routes with Access Control
Below is an example illustrating how to configure route access control using the privileges property:
import { RemateRoutesType } from '@remate/core';
const routes: RemateRoutesType<string> = [
// Route accessible only by authenticated user
{
path: '/',
element: <h1>Home Page</h1>,
authenticated: true
},
// Route accessible only by authenticated user and admin role
{
path: '/admin-dashboard',
element: <h1>Admin Dashboard</h1>,
privileges: 'admin' // Required privilege for this route
},
// Grouped routes for authentication pages (e.g., Sign-In and Sign-Up)
{
authenticated: false,
layout: {
style: 'raw' // Optional layout configuration
},
isNavigable: false, // Indicates these routes are not part of the main navigation
children: [
{
path: '/sign-in',
element: <h1>Sign In Page</h1> // Public sign-in page
},
{
path: '/sign-up',
element: <h1>Sign Up Page</h1> // Public sign-up page
}
]
}
];Authenticated Component
The Authenticated component is a powerful utility for conditionally rendering pages or components based on user authenticated status.
How It Works
- Authenticated Check:
check user is authenticated - Fallback Rendering:
If the user does not authenticated, the content provided in thefallbackprop is rendered instead.
Props
fallback(ReactNode | null):
Content to render when the user is not authorized. Defaults tonull.
Example: Basic Usage
Below is a simple example of using the Authenticated component to manage access control on a page:
import { Authenticated } from '@remate/core';
export const ListPage = () => {
return (
<Authenticated fallback={<button>sign in</button>}>
<>
<h1>User info</h1>
...
</>
</Authenticated>
);
};CanAccess Component
The CanAccess component is a powerful utility for conditionally rendering pages or components based on user privileges. It integrates seamlessly with the canAccess method provided by the useAuth hook to determine whether a user has the necessary permissions.
How It Works
Privilege Check:
Theprivilegesprop specifies the required privilege(s) for accessing the component.Fallback Rendering:
If the user does not have the required privileges, the content provided in thefallbackprop is rendered instead.Nested Access Control:
TheCanAccesscomponent supports nesting, allowing granular control over different sections of your UI.
Props
privileges(TRequestedPrivileges = any):
Specifies the required privilege(s). It can be a single string or an array of privileges.fallback(ReactNode | null):
Content to render when the user is not authorized. Defaults tonull.
Example: Basic Usage
Below is a simple example of using the CanAccess component to manage access control on a page:
import { CanAccess } from '@remate/core';
export const ListPage = () => {
return (
<CanAccess
privileges="user"
fallback={<h1>You are not authorized to see this page.</h1>}
>
<>
<h1>Products</h1>
<CanAccess privileges="admin">
<Button>See Details</Button>
</CanAccess>
</>
</CanAccess>
);
};Explanation of the Example
Outer
CanAccess:- Ensures that only users with the
userprivilege can see theProductspage. - If the user lacks
userprivileges, the fallback message ("You are not authorized to see this page.") is displayed.
- Ensures that only users with the
Nested
CanAccess:- Within the
Productspage, access to the "See Details" button is restricted to users with theadminprivilege.
- Within the
Layouts
Remate features a powerful layout system that allows you to configure and use different layouts for each route. This flexibility makes it easy to have specialized pages, such as a login page without a toolbar or navbar, while keeping the rest of the application consistent.
To start using the layout system, you need to define your application's layouts and pass them to the Remate component as key-value pairs. Here’s how you can do it:
Defining Layouts
The following example demonstrates how to define layouts and pass them to the Remate component:
import { Remate, ILayoutProps } from '@remate/core';
import { BrowserRouter } from 'react-router';
function DefaultLayoutComponent({ children, config }: ILayoutProps) {
// Custom configuration from route config
if (config.raw) {
return children;
}
return <main>{children}</main>;
}
const myLayouts = {
default: ({ config: configFromRouteConfig, children }: ILayoutProps) => (
<DefaultLayoutComponent config={configFromRouteConfig}>{children}</DefaultLayoutComponent>
)
// Additional layouts can be defined here...
};
function App() {
return (
<BrowserRouter>
<Remate
// Passing layouts to the system
layouts={myLayouts}
// Specifying the default layout
// to use when no layout is defined in route config
defaultLayoutStyle="default"
>
{/* Children components */}
</Remate>
</BrowserRouter>
);
}
export default App;Layout Configuration for Routes
Once you have defined and passed layouts to the system, you can configure layouts for specific routes as needed. This configuration is hierarchical, meaning a parent route’s layout configuration applies to its children unless overridden by a child’s layout configuration.
Layout Configuration Options
- Style: Specifies which layout component to render.
- Config: An object that passes custom data to the layout component. For example, you can send a flag to hide the toolbar or navbar on a specific page.
Example: Configuring Layouts for Routes
The following example demonstrates how to configure layouts for routes:
import { RemateRoutesType } from '@remate/core';
import Homepage from './pages/Home';
import SignInPage from './pages/SignIn';
const routes: RemateRoutesType = [
{
path: '/',
element: <HomePage />,
layout: {
style: 'homeLayout'
}
},
{
path: '/signin',
element: <SignInPage />,
layout: {
config: {
raw: true
}
}
}
];Key Points
- Default Layout: The
defaultLayoutStyleproperty defines the layout to use if no layout is specified in the route configuration. - Hierarchical Configuration: Layout configurations are inherited by child routes unless explicitly overridden. When overridden, configurations are merged hierarchically.
- Custom Configurations: The
configobject allows you to pass any data to the layout component, enabling highly customizable layouts.
Routing
Remate leverages a custom routing system built on top of the robust React Router v7. This system enables modular and flexible management of application routes, ensuring that your routing strategy is highly adaptable to various application requirements.
Key Features
- Flexible Authorization: Easily enforce route-level permissions using the
privilegesproperty. - Dynamic Layouts: Customize layouts for individual routes or groups of routes using the
layoutconfiguration. - Dynamic Titles: Seamlessly manage document titles and breadcrumbs with the
titleanddynamicTitleproperties. - Nested Routing: Define parent and child routes with cascading configurations for layouts and metadata.
- Custom Metadata: Add meta properties to routes for extended functionality, such as analytics or custom behaviors.
- Navigation Stack State: Access the current navigation state to build breadcrumbs or navigators dynamically.
This system ensures that you have full control over how routes are structured and managed within your Remate application.
Note: Authorization (
privileges) and layout settings are hierarchically merged with child routes, but child route configurations take precedence over parent settings.
Defining Routes
To define the routes for your application, you provide an array of route objects to the <Remate /> component. These route objects use a custom type, RemateRouteItemType, which extends the base RouteObject type from React Router.
Route Object Structure
The RemateRouteItemType extends RouteObject by introducing several additional properties to enhance route configuration:
authenticated(Optional): Indicates whether the route requires the user to be authenticated or not.privileges(Optional): Specifies the required privileges a user must have to access the route.isNavigable(Optional): Indicates whether the route should appear in the navigation stack. Defaults totrue.title(Optional): Sets the title for the route, which is used for document titles and breadcrumbs.id(Optional): A unique identifier for the route, useful for manipulating the navigation stack.dynamicTitle(Optional): Enables dynamic title handling. Iftrue, the loading indicator is shown until the title is resolved. Defaults tofalse.layout(Optional): Defines layout configuration for the route.meta(Optional): Provides metadata for the route.
Example: Defining Routes
Here’s an example of how to define application routes using RemateRouteItemType:
import { Navigate } from 'react-router';
import { authPrivileges, RequiredPrivilegesType } from '@auth';
import Error404Page from '@main/404/Error404Page';
import ExampleConfig from '@main/example/ExampleConfig';
import SignInConfig from '@main/sign-in/SignInConfig';
import SignUpConfig from '@main/sign-up/SignUpConfig';
import { RemateRoutesType } from '@remate/core';
/**
* The routes of the application.
*/
const routes: RemateRoutesType<RequiredPrivilegesType> = [
{
path: '/',
element: <Navigate to="/example" />,
authenticated: true
},
{
layout: {
config: {
navbar: { display: false },
toolbar: { display: false },
footer: { display: false },
leftSidePanel: { display: false },
rightSidePanel: { display: false }
}
},
isNavigable: false,
authenticated: false,
children: [...SignInConfig, ...SignUpConfig]
},
...ExampleConfig,
{
path: '*',
authenticated: true,
title: 'PAGE_NOT_FOUND',
isNavigable: false,
element: <Error404Page />
}
];
export default routes;Integrating Routes with <Remate />
After defining your routes, pass them to the <Remate /> component within the application’s root component. Here’s an example:
import { BrowserRouter } from 'react-router';
import { Remate } from '@remate/core';
import routes from './routes';
function App() {
return (
<BrowserRouter>
<Remate
routes={routes}
// Additional props can be passed here
>
{/* Children components */}
</Remate>
</BrowserRouter>
);
}
export default App;Routing Hooks
Remate provides a set of powerful routing hooks that can be used in pages, layouts, or any child components of the <Remate /> component. These hooks offer various routing-related capabilities, enabling developers to manage navigation and state efficiently.
Below is an explanation of these hooks, with examples for better understanding.
Example Configuration for Reference
Here is an example route configuration to provide context for the following hook explanations:
import { lazy } from 'react';
import { authPrivileges } from '@auth';
import { RemateRoutesType } from '@remate/core';
const Example = lazy(() => import('./Example'));
const ExampleDetailsPage = lazy(() => import('./ExampleDetailsPage'));
const ExampleNestedPage = lazy(() => import('./ExampleNestedPage'));
/**
* The Example page config.
*/
const ExampleConfig: RemateRoutesType = [
{
privileges: authPrivileges.user,
path: 'example',
title: 'example page',
id: 'example-main',
children: [
{
index: true,
element: <Example />
},
{
path: ':id',
dynamicTitle: true,
id: 'example-details',
children: [
{
index: true,
privileges: authPrivileges.staff,
element: <ExampleDetailsPage />
},
{
path: 'nested',
element: <ExampleNestedPage />,
title: 'EXAMPLE_NESTED',
privileges: authPrivileges.admin,
id: 'example-nested'
}
]
}
]
}
];
export default ExampleConfig;useMatched
Returns all routes that match the current pathname. If the current route has a parent route, the parent is also included, providing a hierarchical path from the root to the current route.
Return Type:
An array of type RemateRouteMatchType.
Example Usage:
Suppose the current pathname is /example/1/nested.
import { useMatched } from '@remate/core';
export default function ExampleNestedPage() {
const matchedRoutes = useMatched();
console.log(matchedRoutes); // Outputs the matched routes hierarchy
return 'Example Nested Page';
}useRoutes
Returns all defined routes in the application.
Return Type:
An array of type RemateRouteItemType.
useNavigationStackState
Provides the current navigation stack state, which includes only navigable routes. Routes with index: true, path: '', or isNavigable: false are excluded.
Return Type:
An array of type INavigationStackStateItem.
Use Case: Ideal for building breadcrumbs, navigator buttons (e.g., Back), logging, or analytics.
Example Usage:
Suppose the current pathname is /example/1/nested.
import { useNavigationStackState } from '@remate/core';
export default function ExampleNestedPage() {
const navigationStackState = useNavigationStackState();
console.log(navigationStackState); // Outputs the current navigation stack
return 'Example Nested Page';
}useCurrentNavigationStackStateItem
Returns the last item in the navigation stack, representing the current route.
Example Usage: To display the title of the current page in the toolbar:
export default function Toolbar() {
const currentNavigationStackItem = useCurrentNavigationStackStateItem();
return (
<div>
<h1>{currentNavigationStackItem?.title}</h1>
</div>
);
}useNavigationStackStateItemById
Allows selecting a specific item from the navigation stack state by its id. If the item does not exist, it returns null.
Input:
id(string): The ID of the route to retrieve.
Return Type:
INavigationStackStateItem | null
Example:
import { useNavigationStackStateItemById } from '@remate/core';
export default function ExamplePage() {
const stackItem = useNavigationStackStateItemById('example-details');
console.log(stackItem);
return 'Example Page';
}useSetDynamicTitle
Used for routes with dynamic titles, typically when the title is determined after fetching data from an API. This hook updates the title in the navigation stack state automatically.
Example Usage:
import { useEffect, useState } from 'react';
import { useNavigationStackStateItemById, useSetDynamicTitle } from '@remate/core';
function ExampleDetailsPage() {
const { id } = useParams();
const [dynamicTitle, setDynamicTitle] = useState<string | null>(null);
const currentStackItem = useNavigationStackStateItemById('example-details');
// Simulate fetching data from an API
useEffect(() => {
setTimeout(() => {
setDynamicTitle(`Example details ${id} dynamic title`);
}, 1000);
}, [id]);
useSetDynamicTitle('example-details', dynamicTitle);
return <div className="p-24">{currentStackItem?.title ? <h4>{currentStackItem.title}</h4> : 'loading'}</div>;
}
export default ExampleDetailsPage;Routing Actions
In this section, we explore methods that allow you to perform changes on the navigation state and routes in your application. These actions provide dynamic control over routing behaviors, enabling better adaptability to complex navigation scenarios.
resetNavigationStackState
The resetNavigationStackState action resets the navigation stack state.
- Parameters:
routeId(optional): If provided, only the specified stack item will be reset.
This method is useful when you want to clear navigation states for a route or the entire stack.
Example Usage
import { useEffect, useState } from 'react';
import { useNavigationStackStateItemById, useSetDynamicTitle, resetNavigationStackState } from '@remate/core';
function ExampleDetailsPage() {
const { id } = useParams();
const [dynamicTitle, setDynamicTitle] = useState<string | null>(null);
const currentStackItem = useNavigationStackStateItemById('example-details');
// Simulate fetching data from an API
useEffect(() => {
setTimeout(() => {
setDynamicTitle(`Example details ${id} dynamic title`);
}, 1000);
}, [id]);
useSetDynamicTitle('example-details', dynamicTitle);
return (
<div className="p-24">
{currentStackItem?.title ? (
<>
<h4>{currentStackItem.title}</h4>
<button
onClick={() => {
resetNavigationStackState();
// Reset navigation state for a specific route
// resetNavigationStackState('example-details');
}}
>
Reset Navigation State
</button>
</>
) : (
'Loading...'
)}
</div>
);
}
export default ExampleDetailsPage;setNavigationStackStateItemSetting
The setNavigationStackStateItemSetting action allows you to modify the settings of a navigation stack state item.
- Key Features:
- Can be used both before and after a stack item exists in the navigation state.
- Overrides default settings of the stack state item.
Example Usage
import { useNavigationStackStateItemById, setNavigationStackStateItemSetting } from '@remate/core';
function ExampleDetailsPage() {
const currentStackItem = useNavigationStackStateItemById('example-details');
return (
<div className="p-24">
<h4>{currentStackItem?.title || 'Loading...'}</h4>
<button
onClick={() => {
setNavigationStackStateItemSetting('example-details', {
title: 'New Route Title'
// Additional settings can be modified here
});
}}
>
Set Navigation State
</button>
</div>
);
}
export default ExampleDetailsPage;Notifications
Notifications and visual feedback play a crucial role in enhancing user experience within an application. Remate provides a notification integration system that operates seamlessly in scenarios such as failed requests, successful form submissions, or other key events.
Notification Provider
Remate allows you to configure a custom notification API by passing a notificationProvider to the <Remate /> component.
The notificationProvider is an object that includes two essential methods:
open: Displays a notification with specified parameters.close: Hides a notification identified by its key.
These methods can be accessed and invoked from anywhere in the application using the useNotification hook, ensuring flexibility and ease of integration.
Structure of notificationProvider
A notificationProvider must implement the following methods:
const notificationProvider = {
open: (params: OpenNotificationParams) => {},
close: (key: string) => {}
};The associated type definitions for these methods are:
interface NotificationProvider {
open: (params: OpenNotificationParams) => void;
close: (key: string) => void;
}
interface OpenNotificationParams {
key?: string;
type: 'success' | 'error' | 'warning' | 'info';
message: string;
description?: string;
}Integrating notificationProvider
To enable notifications in Remate, pass your custom notificationProvider object to the <Remate /> component.
Below is an example using react-toastify as the notification provider:
import { toast } from 'react-toastify';
import { Remate, NotificationProviderType } from '@remate/core';
const notificationProvider: NotificationProviderType = {
open: ({ key, message, type }) => {
if (toast.isActive(key as string)) {
toast.update(key as string, {
render: message,
closeButton: true,
autoClose: 5000,
type
});
} else {
toast(message, {
toastId: key,
type
});
}
},
close: (key) => toast.dismiss(key)
};
const App = () => {
return (
<Remate
notificationProvider={notificationProvider}
/* ...other props */
>
{/* App components */}
</Remate>
);
};useNotification Hook
The useNotification hook provides access to the open and close methods from the configured notificationProvider. This hook allows you to trigger notifications dynamically from any part of your application.
Usage Example
Below is an example demonstrating how to use the useNotification hook:
import { useNotification } from '@remate/core';
const NotificationExample = () => {
const { open, close } = useNotification();
// Trigger a success notification
const handleOpen = () => {
open({
key: 'notification-key',
type: 'success',
message: 'Success',
description: 'This is a success message'
});
};
// Dismiss a notification
const handleClose = () => {
close('notification-key');
};
return (
<div>
<button onClick={handleOpen}>Show Notification</button>
<button onClick={handleClose}>Hide Notification</button>
</div>
);
};
export default NotificationExample;Data Fetching
Data fetching is a crucial component of any UI application, enabling seamless interaction between users and underlying data sources. UI applications act as intermediaries, allowing users to interact with and manipulate data in meaningful ways.
Overview
Remate leverages @tanstack/react-query v5 under the hood to handle data fetching. It introduces a unique syntax for defining endpoints, enabling reusability and separation of concerns. This system supports diverse data-fetching mechanisms, such as RESTful APIs, with remarkable flexibility and scalability.
Key Features
- Integration with React Query: Provides built-in caching and data fetching mechanisms based on React Query.
- UI Layer Agnostic: The functionality can be used with any UI layer.
- Predefined API Endpoints: Endpoints are defined ahead of time, specifying query parameters and response transformations for caching.
- React Hook Generation: Automatically generates React hooks to encapsulate data-fetching logic, including
dataandisLoadingstates. - Error Handling: Supports UI error notifications and retry mechanisms using error boundaries.
- TypeScript Support: Fully written in TypeScript for an excellent development experience.
- Extensibility: Highly customizable and extensible while leveraging all features of React Query.
- Reusable Hooks: Generates reusable hooks for each endpoint, promoting separation of concerns by decoupling data-fetching logic from React components.
Getting Started
Using Remate’s data-fetching system is straightforward:
- Define your API service with custom logic and types using the
createApimethod. - Use the
buildEndpointsmethod of created apiService withcreateApito define endpoints such asquery,mutation, andinfiniteQuery. - Leverage the generated hooks in your UI components.
Example Usage
Wrapping the Application with QueryClientProvider
To start, wrap your application with the QueryClientProvider from @tanstack/react-query and include the Remate component:
import { Remate } from '@remate/core';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
const queryClient = new QueryClient();
const App = () => {
return (
<QueryClientProvider client={queryClient}>
<Remate>{/* Application Components */}</Remate>
</QueryClientProvider>
);
};Defining Base Query Logic
You can define the base query logic like using Axios:
import { BaseQueryFn, createApi } from '@remate/core';
import axios, { AxiosError, AxiosRequestConfig } from 'axios';
const axiosBaseQuery =
(): BaseQueryFn<string | AxiosRequestConfig, unknown, AxiosError> =>
async (args, { signal }) => {
try {
const result = await axios({ signal, ...(typeof args === 'string' ? { url: args } : args) });
return { data: result.data };
} catch (error) {
return { error: error as AxiosError };
}
};
export const apiService = createApi({
baseQuery: axiosBaseQuery()
});Creating Endpoints
Define endpoints using the buildEndpoints method of your apiService and generate hooks for use in components:
const { useCreateUserMutation, useGetInfiniteUsersInfiniteQuery, useGetUsersQuery } = apiServi