How to Implement Dynamic React Feature Flags (2025 Update)
- Share:
Feature flags can enable different users to see or interact with specific features in your React application based on conditions like user role, location, or relationship to content.
With the dynamic and often complex nature of modern React applications, Implementing feature flags in React is an essential capability for basically any application you might be building. That means that we, as software developers, need its implementation to be dynamic, intuitive, and easy to manage.
This guide will walk you through building a React feature toggle system using CASL, an open-source JavaScript authorization library, and Permit.io. By leveraging React authorization with Relationship-Based Access Control (ReBAC), we’ll dynamically manage feature visibility based on user-resource relationships.
What We'll Build
In this tutorial, we’ll build a basic document management app that uses feature flags to control access to various document-related features. Users with specific relationships to documents—such as Document Owners or users with Category Access—will have different permissions to create, view, or edit documents. This app dynamically adapts feature visibility and actions based on user relationships, making it a perfect example of a flexible and scalable feature flagging approach in React.
Why Use React Feature Flags?
Feature flags give developers control over which features users can access. Whether testing new features, running A/B tests, or personalizing user experiences, feature flags provide the flexibility to enable or disable functionality without modifying code.
In React, feature flags can be integrated with an authorization system to restrict access in a dynamic fashion. Instead of relying on static role-based access control (RBAC), we’ll implement relationship based access control (ReBAC) with Permit.io to manage permissions at a more granular level based on relationships between users and resources.
Before We Start:
To follow this tutorial, you’ll need to create an account with Permit.io account and configure a local Policy Decision Point (PDP) with a valid API key (Which we’ll show you how to do)
The demo app code is available in this GitHub repository.
By the end, you’ll have a clear understanding of how to create dynamic feature flags that adapt based on user relationships using CASL and Permit.io in React.
Modeling ReBAC Policies
The first step to enabling dynamic feature flags is defining the policies that will enforce them. Feature flags do not exist in isolation—they are controlled by permission checks that determine whether a user can access or interact with a feature. ReBAC helps structure these permissions by modeling relationships between users and resources.
To model this permission management layer, we’ll start by defining resources:
Defining Resources
For this document manager app, we will create two resources: Category and Document. Think of a category as a folder and document as files in the folder. For instance, finance is a category, and expense_budget
and salary_expenses
are documents in the finance category.
Defining resources in Permit.io’s no-code ReBAC UI
Next, we define actions that a user can act on the Category and Document resources.
For Category:
list-documents
,create-document
,delete
, andrename
For Document:
comment
,edit
,delete
,read
Defining Resource Roles
A resource role denotes the set of permissions that can be granted to instances of a particular resource. In our case, the Category resource will have the Editor, Admin, and Viewer roles.
The document resource will have Editor and Viewer roles.
Defining Permissions
Permissions determine what actions a resource role (e.g., Category#Editor
) can perform. These permissions will later be used as feature flags, dynamically enabling or disabling access to specific UI components and actions based on the user’s role.
Let’s define what action a resource role (e.g., Category#Editor
) is permitted to do:
A Category#Admin
, in our case, has the permission to:
create-document
delete
list-documents
rename
A Document#Editor has permission to:
comment
edit
read
- but no permission to
delete
Defining Relations
In Permit, a relation is a type of relationship Tuple. In our case, we have just one relation. Category is a parent of Document. Learn more about Relationship Tuples here.
Defining Role Derivations
In ReBAC, role derivation lets users gain permissions without needing a direct role assignment for each instance. As a result, a Category#Editor
will also be a Document#Editor
when a Category instance is the parent of a Document instance. This means a user with a Category#Editor
role has access to edit all documents in the finance category even without explicitly defining that permission.
Same with a Category#Viewer
It’s important to note that this derivation is based on the relation. For instance, a Document#Editor
can not be a Category#Editor
.
This is what our model looks like so far.
Defining Resource Instances and Relationship Tuples
Let’s define some resource instances. As the name implies, a resource instance is a specific occurrence or version of a resource. In our case, we create finance and HR as resource instances of Category. Then, budget_report
, marketing_expense
, and salary_report
are resource instances of Document.
Now let’s define relationships between these instances. As defined, the finance category has access to budget_report
and marketing_expense
documents. This is cool because the resource role derivation and relations have established this connection.
The HR category has access to the salary_report
document
Assigning User to Resource Instance
Note: In a real-world application, the role assignment happens programmatically using Permit’s SDKs.
To assign a user to a resource instance, you have to pass the userId
, role
, tenant
, and resource_instance
to the roleAssignments
endpoint.
// Assign user as editor of a category
await permit.api.roleAssignments.assign({
user: userId,
role: "Editor", // Becomes Category#Editor
tenant: "default",
resource_instance: `Category:${categoryId}`
});
Here’s an example:
This admin user has instance access to all categories and documents in the app.
At this point, your ReBAC system is ready.
However, the goal of this guide is to use this to implement feature toggling. So our next step is to write some API endpoints that use permit.check
to determine if a user can perform certain actions or have access to resources.
Setting up Local PDP and Initializing Permit SDK
In a modern fine-grained authorization system, policies can become highly dynamic and context-sensitive, relying on various attributes, roles, and relationships between users and resources.
To evaluate these policies effectively, a Policy Decision Point (PDP) is required. The PDP is responsible for making real-time decisions on whether a user is allowed to perform certain actions on a resource based on the relationships and rules defined within the access control system.
For this project, we use a Local PDP to handle the more complex ReBAC policies.
You can follow Permit.io's official documentation to set up your Local PDP using Docker. Below is a screenshot showing the steps to pull and run the PDP container:
Once the PDP container is running, it will listen on port 7766
and will handle incoming requests for permission checks. The PDP container communicates with Permit.io's API and processes the ReBAC policies that define access control based on user-resource relationships.
Next, in our project, we connect the backend to this PDP. This is done in the permitInstance.js
file where the Permit.io SDK is initialized:
require('dotenv').config({ path: ".env" });
const { Permit } = require("permitio");
const permit = new Permit({
token: process.env.PERMIT_TOKEN,
pdp: "<http://localhost:7766>",
debug: true,
log: {
level: "debug"
}
});
module.exports = permit;
This setup ensures that any permission checks made through the backend will now be processed by the local PDP.
Creating API Endpoints to Handle Permission Checks
Now that we have configured the policies that will determine our feature flags, we can start handling the flagging itself with permission checks. The API will check if a user has the necessary permissions to perform a specific action on a resource. Instead of explicitly assigning access per user, we rely on relationships and role derivations to determine feature access dynamically.
We need to specify some endpoints to check if a specific user has the necessary permissions to perform a particular action on a given resource.
This is where we can really see the advantages of ReBAC - instead of explicitly assigning access to documents per user, we can simply assign a user to the relevant category, and document access will be assigned automatically.
For the scope of this article, we’ll only check if a user has the necessary permissions to categories. However, you can go ahead and explore the endpoint implementation for documents here.
router.get('/', async (req, res) => {
try {
const userId = req.headers['user-id'];
const accessibleCategories = await Promise.all(
Object.values(categories).map(async (category) => {
const canAccess = await permit.check(userId, "list-documents", `Category:${category.id}`);
if (!canAccess) return null;
// Add document count
const categoryDocuments = Object.values(documents)
.filter(doc => doc.categoryId === category.id);
return {
...category,
documentCount: categoryDocuments.length
};
})
);
res.json(accessibleCategories.filter(Boolean));
} catch (error) {
res.status(500).json({ error: error.message });
}
});
This endpoint determines which categories a user has access to by using the permit.check
function to verify permissions based on relationships. This endpoint returns only the categories where the user has the list-documents
permission, along with a count of documents in each accessible category.
Integrating CASL with permit-fe-sdk
CASL allows us to enforce granular permissions within our app, ensuring that resources and actions are accessible only to authorized users or roles.
For our React frontend, we use the permit-fe-sdk
to implement permission-based feature flags. Instead of making individual permission checks, we load all necessary permissions when the app initializes using loadLocalStateBulk
. This optimizes performance by reducing the number of API calls to our permission server.
// lib/permit.ts
import { Permit, permitState } from 'permit-fe-sdk';
export const getAbility = async (userId: string) => {
console.log('Initializing permit for user:', userId);
const permit = Permit({
loggedInUser: userId,
backendUrl: "<http://localhost:3001>"
});
await permit.loadLocalStateBulk([
// Category permissions with instance keys
{ action: "list-documents", resource: "Category:finance" },
{ action: "list-documents", resource: "Category:hr" },
{ action: "create-document", resource: "Category:finance" },
{ action: "create-document", resource: "Category:hr" },
// Document permissions with instance keys
{ action: "read", resource: "Document:budget_report" },
{ action: "read", resource: "Document:marketing_expense" },
{ action: "read", resource: "Document:salary_report" },
{ action: "edit", resource: "Document:budget_report" },
{ action: "edit", resource: "Document:marketing_expense" },
{ action: "edit", resource: "Document:salary_report" },
]);
console.log('Permissions loaded into permitState');
};
export { permitState };
Notice how we specify the exact resource instances (e.g., Category:finance
) rather than just resource types. This matches our ReBAC model where permissions are based on relationships to specific resource instances.
Conditionally Render the UI Based on Feature Flags with permitState
On the Category page, we use permitState.check
to verify if the logged-in user has access to the list of documents in a specific category. This permission check determines whether to show the documents or display an access denied message.
// pages/categories/[categoryId].tsx
useEffect(() => {
const loadData = async () => {
if (!isLoaded || !user || !categoryId || !permissionsLoaded) return;
// Check if user can access this category
const canAccess = permitState?.check(
"list-documents",
`Category:${categoryId}`,
{},
{}
);
if (!canAccess) {
setAccessError(`You don't have access to view documents in this category`);
return;
}
// Load documents if user has access
setIsLoading(true);
try {
const docs = await fetchCategoryDocuments(categoryId as string, user.id);
setDocuments(docs);
setCategoryName(categoryId === 'finance' ? 'Finance' : 'HR');
} catch (error) {
console.error('Error loading documents:', error);
} finally {
setIsLoading(false);
}
};
loadData();
}, [categoryId, isLoaded, user, permissionsLoaded]);
When a user navigates to a category page, we check their permission before loading any data. If they don't have access, they see an error message instead of the documents list. Now, users only see categories and documents they’re authorized to access based on React feature flags.
That's It for Now!
Using React feature flags with CASL and Permit.io enables real-time, relationship-based access control. Instead of static roles, we assign permissions dynamically based on user-resource relationships. This ensures fine-grained React authorization, making feature toggling more secure and adaptable to complex use cases.
Using the permit-fe-sdk
, we implemented these complex permission checks with simple, intuitive code. The ability to load permissions in bulk and check them using permitState.check()
makes it straightforward to toggle features based on user relationships without compromising on security or performance.
By integrating feature flags in React, you can personalize user experiences, control feature rollouts, and manage permissions efficiently—without redeploying code.
Looking to learn more? Join our Slack community to discuss React authorization and feature flags with other developers.
Written by
Gabriel L. Manor
Full-Stack Software Technical Leader | Security, JavaScript, DevRel, OPA | Writer and Public Speaker