@arnosaine/is

中文文档
0.2.9发布于 2025/01/23ISCTypeScriptES Module近 30 天下载 3409

Feature Flags, Roles and Permissions-based rendering, A/B Testing, Experimental Features, and more in React.

原始使用文档
文档目录28 节
  1. Key Features
  2. Contents
  3. Demos
  4. Getting Started
  5. Create <Is> & useIs
  6. Use <Is> & useIs
  7. Ideas
  8. Feature Flags
  9. Application Variants by the Domain
  10. User Roles and Permissions
  11. Is a Specific Day
  12. Shortcut Components and Hooks
  13. Loader (React Router / Remix)
  14. Setup
  15. Using loadIs
  16. Utilities
  17. API
  18. create
  19. createFromLoader
  20. <Is>
  21. useIs
  22. loadIs
  23. is
  24. toBooleanValues
  25. Types
  26. Value
  27. Values
  28. Conditions

@arnosaine/is

Feature Flags, Roles and Permissions-based rendering, A/B Testing, Experimental Features, and more in React.

Key Features

  • Declarative syntax for conditionally rendering components
  • Support for various data sources, including context, hooks, and API responses
  • Customizable with default conditions and dynamic values

Create a custom <Is> component and useIs hook for any conditional rendering use cases.

Or create shortcut components like <IsAuthenticated>, <HasRole> / <Role> and <HasPermission> / <Can>, and hooks like useIsAuthenticated, useHasRole / useRole and useHasPermission / useCan, for the most common use cases.

If you are using React Router or Remix, use createFromLoader to also create loadIs loader and utility functions like authenticated.

Contents

Demos

Getting Started

Here, we create a component and a hook to check if the user is authenticated or if experimental features are enabled. We get the user from UserContext. Experimental features are enabled on preview.* domains, for example, at http://preview.localhost:5173.

Create <Is> & useIs

./is.ts:

import { create } from "@arnosaine/is";
import { use } from "react";
import UserContext from "./UserContext";

const [Is, useIs] = create(function useValues() {
  const user = use(UserContext);

  const isExperimental = location.hostname.startsWith("preview.");

  // Or, get the value from the user context, a hook call, or another
  // source.
  // const isExperimental = user?.roles?.includes("developer") ?? false;

  return {
    // The property names become the prop and hook param names.
    // Allowed types: boolean | number | string | boolean[] | number[] |
    // string[].
    authenticated: Boolean(user),
    experimental: isExperimental,
    // ...
  };
});

export { Is, useIs };

Use <Is> & useIs

import { Is, useIs } from "./is";

// Component

<Is authenticated fallback="Please log in">
  Welcome back!
</Is>;

<Is experimental>
  <SomeExperimentalFeature />
</Is>;

// Hook

const isAuthenticated = useIs({ authenticated: true }); // boolean
const isExperimental = useIs({ experimental: true }); // boolean

ℹ️ Consider lazy loading if the conditional code becomes large. Otherwise, the conditional code is included in the bundle, even if it's not rendered. Additionally, do not use this method if the non-rendered code should remain secret.

Ideas

Feature Flags

Hardcoded Features

A list of hardcoded features is perhaps the simplest method and can still improve the project workflow. For example, some features can be enabled in the release branch, while different features can be enabled in the main or feature branches.

./is.ts:

import { create } from "@arnosaine/is";

const [Is, useIs] = create(function useValues() {
  return {
    // Hardcoded features
    feature: ["feature-abc", "feature-xyz"] as const,
    // ...
  };
});

export { Is, useIs };

Build Time Features

Read the enabled features from an environment variable at build time:

.env:

FEATURES=["feature-abc","feature-xyz"]

./is.ts:

import { create } from "@arnosaine/is";

const [Is, useIs] = create(function useValues() {
  return {
    // Read the enabled features from an environment variable at build
    // time
    feature: JSON.parse(import.meta.env.FEATURES ?? "[]"),
    // ...
  };
});

export { Is, useIs };

Runtime Features

Read the enabled features from a config file or an API at runtime:

public/config.json:

{
  "features": ["feature-abc", "feature-xyz"]
}

./is.ts:

import { create } from "@arnosaine/is";
import { use } from "react"; // React v19

async function getConfig() {
  const response = await fetch(import.meta.env.BASE_URL + "config.json");
  return response.json();
}

const configPromise = getConfig();

const [Is, useIs] = create(function useValues() {
  const config = use(configPromise);

  return {
    feature: config.features,
    // ...
  };
});

export { Is, useIs };

A/B Testing, Experimental Features

Enable some features based on other values:

./is.ts:

import { create } from "@arnosaine/is";

const [Is, useIs] = create(function useValues() {
  const features = [
    /*...*/
  ];

  // Enable some features only in development mode:
  if (import.meta.env.MODE === "development") {
    features.push("new-login-form");
  }

  // Or, enable some features only on `dev.*` domains, for example, at
  // http://dev.localhost:5173:
  if (location.hostname.startsWith("dev.")) {
    features.push("new-landing-page");
  }

  return {
    feature: features,
    // ...
  };
});

export { Is, useIs };

Enable All Features in Preview Mode

./is.ts:

import { create } from "@arnosaine/is";

const [Is, useIs] = create(function useValues() {
  const features = [
    /*...*/
  ];

  const isPreview = location.hostname.startsWith("preview.");

  return {
    feature: isPreview
      ? // In preview mode, all features are enabled.
        // Typed as string to accept any string as a feature name.
        (true as unknown as string)
      : features,
    // ...
  };
});

export { Is, useIs };

Usage

It does not matter how the features are defined; using the <Is> and useIs is the same:

import { Is, useIs } from "./is";

// Component

<Is feature="new-login-form" fallback={<OldLoginForm />}>
  <NewLoginForm />
</Is>;

// Hook

const showNewLoginForm = useIs({ feature: "new-login-form" });

Application Variants by the Domain

ℹ️ In the browser, location.hostname is a constant, and location.hostname === "example.com" && <p>This appears only on example.com</p> could be all you need. You might still choose to use the Is pattern for consistency and for server-side actions and loaders.

./is.ts:

import { create } from "@arnosaine/is";

const [Is, useIs] = create(function useValues() {
  const domain = location.hostname.endsWith(".localhost")
    ? // On <domain>.localhost, get subdomain.
      location.hostname.slice(0, -".localhost".length)
    : location.hostname;

  return {
    variant: domain,
    // ...
  };
});

export { Is, useIs };

Usage

import { Is, useIs } from "./is";

// Component

<Is variant="example.com">
  <p>This appears only on example.com</p>
</Is>;

// Hook

const isExampleDotCom = useIs({ variant: "example.com" });

User Roles and Permissions

./is.ts:

import { create } from "@arnosaine/is";
import { use } from "react";
import UserContext from "./UserContext";

const [Is, useIs] = create(function useValues() {
  const user = use(UserContext);

  return {
    authenticated: Boolean(user),
    role: user?.roles, // ["admin", ...]
    permission: user?.permissions, // ["create-articles", ...]
    // ...
  };
});

export { Is, useIs };

Usage

import { Is, useIs } from "./is";

// Component

<Is authenticated fallback="Please log in">
  Welcome back!
</Is>;

<Is role="admin">
  <AdminPanel />
</Is>;

<Is permission="update-articles">
  <button>Edit</button>
</Is>;

// Hook

const isAuthenticated = useIs({ authenticated: true });
const isAdmin = useIs({ role: "admin" });
const canUpdateArticles = useIs({ permission: "update-articles" });

Is a Specific Day

./is.ts:

import { create } from "@arnosaine/is";
import { easter } from "date-easter";
import { isSameDay } from "date-fns";

const [Is, useIs] = create(function useValues() {
  return {
    easter: isSameDay(new Date(easter()), new Date()),
    // ...
  };
});

export { Is, useIs };

Usage

import { Is, useIs } from "./is";

// Component

<Is easter>🐣🐣🐣</Is>;

// Hook

const isEaster = useIs({ easter: true });

Shortcut Components and Hooks

import { create } from "@arnosaine/is";
import { use } from "react";
import UserContext from "./UserContext";

const [IsAuthenticated, useIsAuthenticated] = create(
  function useValues() {
    const user = use(UserContext);

    return { authenticated: Boolean(user) };
  },
  { authenticated: true } // Default props / hook params
);

<IsAuthenticated fallback="Please log in">Welcome back!</IsAuthenticated>;

const isAuthenticated = useIsAuthenticated();
import { create, toBooleanValues } from "@arnosaine/is";
import { use } from "react";
import UserContext from "./UserContext";

const [HasRole, useHasRole] = create(function useValues() {
  const user = use(UserContext);

  // Create object { [role: string]: true }
  return Object.fromEntries((user?.roles ?? []).map((role) => [role, true]));
});

<HasRole admin>
  <AdminPanel />
</HasRole>;

const isAdmin = useHasRole({ admin: true });

// Same with toBooleanValues utility
const [Role, useRole] = create(() => toBooleanValues(use(UserContext)?.roles));

<Role admin>
  <AdminPanel />
</Role>;

const isAdmin = useRole({ admin: true });
import { create, toBooleanValues } from "@arnosaine/is";
import { use } from "react";
import UserContext from "./UserContext";

const [HasPermission, useHasPermission] = create(function useValues() {
  const user = use(UserContext);

  // Create object { [permission: string]: true }
  return Object.fromEntries(
    (user?.permissions ?? []).map((permission) => [permission, true])
  );
});

<HasPermission update-articles>
  <button>Edit</button>
</HasPermission>;

const canUpdateArticles = useHasPermission({ "update-articles": true });

// Same with toBooleanValues utility
const [Can, useCan] = create(() =>
  toBooleanValues(use(UserContext)?.permissions)
);

<Can update-articles>
  <button>Edit</button>
</Can>;

const canUpdateArticles = useCan({ "update-articles": true });

For a Very Specific Use Case

import { create } from "@arnosaine/is";
import { use } from "react";
import UserContext from "./UserContext";

const [CanUpdateArticles, useCanUpdateArticles] = create(
  function useValues() {
    const user = use(UserContext);

    return {
      updateArticles: user?.permissions?.includes("update-articles") ?? false,
    };
  },
  { updateArticles: true } // Default props / hook params
);

<CanUpdateArticles>
  <button>Edit</button>
</CanUpdateArticles>;

const canUpdateArticles = useCanUpdateArticles();

Loader (React Router / Remix)

Setup

  1. Create <Is>, useIs & loadIs using createFromLoader.

    ./app/is.ts:

    import { createFromLoader } from "@arnosaine/is";
    import { loadConfig, loadUser } from "./loaders";
    
    const [Is, useIs, loadIs] = createFromLoader(async (args) => {
      const { hostname } = new URL(args.request.url);
      const isPreview = hostname.startsWith("preview.");
      const user = await loadUser(args);
      const config = await loadConfig(args);
    
      return {
        authenticated: Boolean(user),
        feature: config?.features,
        preview: isPreview,
        role: user?.roles,
        // ...
      };
    });
    
    export { Is, useIs, loadIs };
    

    ./app/root.tsx:

  2. Return ...is from the root loader / clientLoader. See options to use other route.

    import { loadIs } from "./is";
    
    export const loader = async (args: LoaderFunctionArgs) => {
      const is = await loadIs(args);
    
      return {
        ...is,
        // ...other loader data...
      };
    };
    

ℹ️ The root ErrorBoundary does not have access to the root loader data. Since the root Layout export is shared with the root ErrorBoundary, if you use <Is> or useIs in the Layout export, consider prefixing all routes with _. (pathless route) and using ErrorBoundary in routes/_.tsx to catch errors before they reach the root ErrorBoundary.

Using loadIs

import { loadIs } from "./is";

// Or clientLoader
export const loader = async (args: LoaderFunctionArgs) => {
  const is = await loadIs(args);

  const isAuthenticated = is({ authenticated: true });
  const hasFeatureABC = is({ feature: "feature-abc" });
  const isPreview = is({ preview: true });
  const isAdmin = is({ role: "admin" });

  // ...
};

Utilities

ℹ️ See Remix example utils/auth.ts and utils/response.ts for more examples.

./app/utils/auth.tsx:

import { loaderFunctionArgs } from "@remix-run/node";
import { loadIs } from "./is";

export const authenticated = async (
  args: LoaderFunctionArgs,
  role?: string | string[]
) => {
  const is = await loadIs(args);

  // Ensure user is authenticated
  if (!is({ authenticated: true })) {
    throw new Response("Unauthorized", {
      status: 401,
    });
  }

  // If the optional role parameter is available, ensure the user has
  // the required roles
  if (!is({ role })) {
    throw new Response("Forbidden", {
      status: 403,
    });
  }
};
import { authenticated } from "./utils/auth";

export const loader = async (args: LoaderFunctionArgs) => {
  await authenticated(args, "admin");

  // User is authenticated and has the role "admin".

  // ...
};

API

create

Call create to declare the Is component and the useIs hook.

const [Is, useIs] = create(useValues, defaultConditions?);

The names Is and useIs are recommended for a multi-purpose component and hook. For single-purpose use, you can name them accordingly. The optional defaultConditions parameter is also often useful for single-purpose implementations.

const [IsAuthenticated, useIsAuthenticated] = create(
  () => {
    // Retrieve the user. Since this is a hook, using other hooks and
    // context is allowed.
    const user = { name: "Example" }; // Example: use(UserContext)
    return { authenticated: Boolean(user) };
  },
  { authenticated: true }
);

Parameters

  • useValues: A React hook that acquires and computes the current values for the comparison logic.
  • optional defaultConditions: The default props/params for Is and useIs.
  • optional options: An options object for configuring the behavior.
    • optional method ("every" | "some"): Default: "some". Specifies how to match array type values and conditions. Use "some" to require only some conditions to match the values, or "every" to require all conditions to match.

Returns

create returns an array containing the Is component and the useIs hook.

createFromLoader

Call createFromLoader to declare the Is component the useIs hook and the loadIs loader.

const [Is, useIs, loadIs] = createFromLoader(loadValues, defaultConditions?, options?);

The names Is, useIs and loadIs are recommended for a multi-purpose component, hook, and loader. For single-purpose use, you can name them accordingly. The optional defaultConditions parameter is also often useful for single-purpose implementations.

const [IsAuthenticated, useIsAuthenticated, loadIsAuthenticated] =
  createFromLoader(
    async (args) => {
      // Retrieve the user. Since this is a loader, using await and
      // other loaders is allowed.
      const user = await loadUser(args);
      return { authenticated: Boolean(user) };
    },
    { authenticated: true }
  );

Parameters

  • loadValues: A React Router / Remix loader function that acquires and computes the current values for the comparison logic.
  • optional defaultConditions: The default props/params for Is, useIs and is.
  • optional options: An options object for configuring the behavior.
    • optional method ("every" | "some"): Default: "some". Specifies how to match array type values and conditions. Use "some" to require only some conditions to match the values, or "every" to require all conditions to match.
    • optional prop: Default: "__is_values". is object (function) property that is expected to be returned in the root loader data.
    • optional routeId: Default: The root route ID ("root" or "0"). The route that provides the is.__is_values from its loader. Example: "routes/admin".

Returns

createFromLoader returns an array containing the Is component, the useIs hook and the loadIs loader.

<Is>

Props

  • ...conditions: Conditions are merged with the defaultConditions and then compared to the useValues / loadValues return value. If multiple conditions are given, all must match their corresponding values. For any array-type condition:
    • If the corresponding value is also an array and options.method is "some" (default), the value array must include at least one of the condition entries. If options.method is "every", the value array must include all condition entries.
    • If the corresponding value is not an array, the value must be one of the condition entries.
  • optional children: The UI you intend to render if all conditions match.
  • optional fallback: The UI you intend to render if some condition does not match.

Usage

<Is authenticated fallback="Please log in">
  Welcome back!
</Is>

<IsAuthenticated fallback="Please log in">Welcome back!</IsAuthenticated>

useIs

Parameters

  • conditions: Conditions are merged with the defaultConditions and then compared to the useValues / loadValues return value. If multiple conditions are given, all must match their corresponding values. For any array-type condition:
    • If the corresponding value is also an array and options.method is "some" (default), the value array must include at least one of the condition entries. If options.method is "every", the value array must include all condition entries.
    • If the corresponding value is not an array, the value must be one of the condition entries.

Returns

useIs returns true if all conditions match, false otherwise.

Usage

const isAuthenticated = useIs({ authenticated: true });
const isAuthenticated = useIsAuthenticated();

loadIs

Parameters

  • args: React Router / Remix LoaderFunctionArgs, ActionFunctionArgs, ClientLoaderFunctionArgs, or ClientActionFunctionArgs.

Returns

loadIs returns a Promise that resolves to the is function.

Usage

export const loader = async (args: LoaderFunctionArgs) => {
  const is = await loadIs(args);
  const authenticated = await loadIsAuthenticated(args);

  const isAuthenticated = is({ authenticated: true });
  const isAuthenticated = authenticated();
  // ...
};

is

is function is the awaited return value of calling loadIs.

Parameters

  • conditions: Conditions are merged with the defaultConditions and then compared to the useValues / loadValues return value. If multiple conditions are given, all must match their corresponding values. For any array-type condition:
    • If the corresponding value is also an array and options.method is "some" (default), the value array must include at least one of the condition entries. If options.method is "every", the value array must include all condition entries.
    • If the corresponding value is not an array, the value must be one of the condition entries.

Returns

is returns a true if all conditions match, false otherwise.

Usage

In root.tsx you must also return ...is from the loader / clientLoader. See options to use other route.

export const loader = async (args: LoaderFunctionArgs) => {
  const is = await loadIs(args);

  return {
    ...is,
    // ...other loader data...
  };
};

toBooleanValues

Call toBooleanValues to convert an array of strings to an object with true values.

const permissionList = [
  "create-articles",
  "read-articles",
  "update-articles",
  "delete-articles",
];
const permissionValues = toBooleanValues(permissions);
// { "create-articles": true, "read-articles": true, ... }

Parameters

  • optional strings: An array of strings.

Returns

toBooleanValues returns an object with true values.

Types

Value

  • Type Value is boolean | number | string.
  • It may also be more specific, like a union of string values.

Example

const features = ["feature-abc", "feature-xyz"] as const;

// "feature-abc" | "feature-xyz"
type Feature = (typeof features)[number];

Values

  • Type Values is Record<string, Value | Value[]>.

Example

{
  "authenticated": true,
  "roles": ["admin"],
  "permissions": [
    "create-articles",
    "read-articles",
    "update-articles",
    "delete-articles"
  ]
}

Conditions

  • Type Conditions is Partial<Values>.

Example

{
  "roles": "admin"
}

版本与发布记录

@arnosaine/is 共发布 14 个版本,首次发布于 2024/05/25;过去 12 个月发布了 0 个版本。

版本发布日期距今
0.2.9latest2025/01/231.8 年前
0.2.82025/01/231.8 年前
0.2.72025/01/171.8 年前
0.2.62024/08/022 年前
0.2.52024/08/022 年前
0.2.42024/07/312 年前

关于 @arnosaine/is 的常见问题

@arnosaine/is 是什么?

Feature Flags, Roles and Permissions-based rendering, A/B Testing, Experimental Features, and more in React.

如何安装 @arnosaine/is?

在项目目录运行 npm install @arnosaine/is,或使用 pnpm add / yarn add / bun add。它还需要宿主项目提供 peer 依赖:react(>=18)、react-dom(>=18)、react-router(>=6)。npm 7 及以上与 pnpm 8 及以上默认会自动安装必需的 peer 依赖,Yarn 需要手动安装。

@arnosaine/is 支持 TypeScript 吗?

支持。包内自带 TypeScript 类型声明(package.json 声明了 types 入口),安装后即可获得类型提示,无需再安装 @types 包。

@arnosaine/is 支持 ESM 还是 CommonJS?

以 ES Module 形式发布,推荐使用 import 引入。在 CommonJS 项目中,Node.js 20.19+ / 22.12+ 可以直接 require() 不含顶层 await 的 ES Module,更早的版本需要改用动态 import()。

@arnosaine/is 可以商用吗?

@arnosaine/is 使用 ISC 许可。宽松许可:允许商业使用、修改和再分发,通常只需在分发时保留原版权与许可声明。以上为概括,不构成法律意见,请以许可原文为准。

@arnosaine/is 的下载量有多少?

根据 npm 官方下载统计,@arnosaine/is 在 2026/10/07 – 2026/11/05 这 30 天内被下载 3,409 次,日均约 114 次。下载量会把 CI 构建、镜像同步和作为其他包依赖的安装一并计入,适合横向比较热度,不能直接等同于使用人数。

@arnosaine/is 还在维护吗?

最新版本 0.2.9 发布于 2025/01/23(1.8 年前);至今共发布 14 个版本,其中过去 12 个月发布了 0 个;首次发布于 2024/05/25。发布频率只是参考信号:功能稳定的工具可能很少需要更新,建议结合源码仓库的 issue 和提交记录判断。