Comparing version 4.4.0 to 5.0.0-alpha.0
{ | ||
"name": "flag", | ||
"version": "4.4.0", | ||
"description": "Feature flagging made easy for React and Redux", | ||
"main": "build/index.js", | ||
"types": "build/index.d.ts", | ||
"version": "5.0.0-alpha.0", | ||
"description": "Strictly typed feature flagging for React", | ||
"main": "dist/index.js", | ||
"module": "dist/index.mjs", | ||
"types": "dist/index.d.ts", | ||
"sideEffects": false, | ||
"repository": "https://github.com/garbles/flag", | ||
@@ -11,40 +13,24 @@ "author": "Gabe Scholz", | ||
"devDependencies": { | ||
"@types/enzyme": "^3.9.1", | ||
"@types/invariant": "^2.2.30", | ||
"@types/jest": "^24.0.11", | ||
"@types/lodash": "^4.14.123", | ||
"@types/react": "^16.8.12", | ||
"@types/react-dom": "^16.8.3", | ||
"@types/react-redux": "^7.0.7", | ||
"enzyme": "^3.9.0", | ||
"enzyme-adapter-react-16": "^1.12.1", | ||
"jest": "^24.7.1", | ||
"np": "^4.0.2", | ||
"react": "^16.8.6", | ||
"react-dom": "^16.8.6", | ||
"react-redux": "^7.0.2", | ||
"ts-jest": "^24.0.1", | ||
"typescript": "^3.5.3" | ||
"@testing-library/react": "^13.0.0", | ||
"@types/jest": "^27.4.1", | ||
"@types/node": "^17.0.23", | ||
"jest": "^27.5.1", | ||
"react": "^18.0.0", | ||
"react-dom": "^18.0.0", | ||
"ts-jest": "^27.1.4", | ||
"tsup": "^5.12.5", | ||
"typescript": "^4.6.3" | ||
}, | ||
"dependencies": { | ||
"deep-computed": "^0.2.0", | ||
"invariant": "^2.2.4", | ||
"lodash": "^4.17.11", | ||
"useful-types": "^0.4.0" | ||
"@types/react": "^18.0.5", | ||
"async-ref": "^0.1.6" | ||
}, | ||
"peerDependencies": { | ||
"@types/react": "^16.8.0", | ||
"@types/react-dom": "^16.8.0", | ||
"react": "^16.8.0", | ||
"react-dom": "^16.8.0", | ||
"react-redux": "^5.0.0 || ^6.0.0 || ^7.0.0", | ||
"redux": "^2.0.0 || ^3.0.0 || ^4.0.0" | ||
"react": "^18" | ||
}, | ||
"scripts": { | ||
"clean": "rm -rf build", | ||
"build": "tsc", | ||
"prepublish": "yarn clean && yarn build", | ||
"test": "jest", | ||
"pub": "np" | ||
"build": "tsup", | ||
"prepublishOnly": "jest && tsc && tsup" | ||
} | ||
} |
299
README.md
# Flag | ||
_Feature flagging made easy for React and Redux_ | ||
:caution: v5 is a work in progress and is not yet published. :caution: | ||
This library aims to offer a best-in-class interface for working with feature flags in TypeScript-based React applications. | ||
``` | ||
yarn add flag | ||
npm install flag | ||
``` | ||
## Motivation | ||
Feature flagging is necessary for large client-side applications. They improve development speed | ||
and allow teams to test new features before they are stable. In order to **WANT** to use feature | ||
flags in an application, they should be **VERY** easy to add and remove. That means minimal | ||
boiler plate and no need to pass boolean props down through component hierarchy. Such a thing could be | ||
done with global variables; however, they live outside of the React/Redux lifecycle, making them | ||
more difficult to control. Instead, this library injects and then accesses feature flags directly | ||
from the React context without getting in your way. | ||
Flag allows you to declare flags as either plain values or as functions. If a flag is a function then it is referred to as a computed flag. The function accepts one argument which is the flags object itself. You do not have to use computed flags, but they can be very convenient. For example, | ||
```ts | ||
const flags = { | ||
// properties can be nested objects | ||
features: { | ||
// they can be boolean | ||
useMyCoolNewThing: true | ||
}, | ||
config: { | ||
// they can be strings | ||
apiUrl: "www.example.com/api" | ||
}, | ||
// they can be numbers | ||
cool: 1, | ||
dude: 5, | ||
// they can be computed | ||
coolAndDude: flags => flags.cool + flags.dude, | ||
// they can be computed from other computed properties. | ||
// other computed properties are resolved for you, so that you do not | ||
// need to call it as a function. | ||
largeCoolAndDude: flags => flags.coolAndDude > 10 | ||
}; | ||
``` | ||
## Getting Started | ||
This library has strong TypeScript support as of v4. In order to get that support, you must | ||
initialize the `flag` library before using it. | ||
`flag` works by creating bindings at runtime so that context providers, components, and hooks are all strictly typed together. `createFlags` builds these bindings without requiring an data. | ||
### createFlags | ||
Creates React bindings for flags. **You should only initialize one instance of this API**. Does | ||
not take any value arguments, but takes one type argument `T` which is the shape of your resolved | ||
flags. | ||
```ts | ||
// flags.ts | ||
import createFlags from "flag"; | ||
import { createFlags } from "flag"; | ||
@@ -73,81 +33,31 @@ export type MyFlags = { | ||
const { FlagsProvider, Flag, useFlag, useFlags } = createFlags<MyFlags>(); | ||
export { FlagsProvider, Flag, useFlag, useFlags }; | ||
export const { FlagBackendProvider, Flag, useFlag } = createFlags<MyFlags>(); | ||
``` | ||
### createReduxBindings | ||
## React Bindings | ||
You can also add support for Redux by importing `createReduxBindings` from `flag/redux`. | ||
### FlagBackendProvider | ||
| Args | Type | Required | Description | | ||
| ---------- | ------------------ | -------- | --------------------------------- | | ||
| `provider` | `FlagsProvider<T>` | `true` | Provider created by `createFlags` | | ||
_Returned as part of `createFlags<T>()`._ | ||
```ts | ||
// flags.ts | ||
This React component provides a `Backend<T>` (see below) as a data source for `Flag` and `useFlag`. | ||
// ... the above | ||
| Props | Type | Required | Description | | ||
| ---------- | ------------------ | -------- | ---------------------- | | ||
| `backend` | `Types.Backend<T>` | `true` | All pre-computed flags | | ||
| `children` | `ReactNode` | `true` | React children | | ||
import createReduxBindings from "flag/redux"; | ||
const { | ||
setFlagsAction, | ||
getFlagsSelector, | ||
createFlagsReducer, | ||
ConnectedFlagsProvider | ||
} = createReduxBindings(FlagsProvider); | ||
export { | ||
setFlagsAction, | ||
getFlagsSelector, | ||
createFlagsReducer, | ||
ConnectedFlagsProvider | ||
}; | ||
``` | ||
## React API | ||
For brevity, the type `T` in the section below refers to the shape of your resolved feature flags. | ||
### Computable | ||
Generic type used to describe unresolved flags. Very useful when including functions are part of your flag definitions because function arguments can be inferred. | ||
```tsx | ||
import { Computable } from "flag"; | ||
type MyFlags = { | ||
a: boolean; | ||
b: boolean; | ||
c: boolean; | ||
}; | ||
const flags: Computable<MyFlags> = { | ||
a: true, | ||
b: false, | ||
// 👇 `flags` type checks! | ||
c: flags => flags.a && flags.b | ||
}; | ||
``` | ||
### FlagsProvider | ||
Returned as part of `createFlags()`. React component that makes flags available to children through the Context API. | ||
| Props | Type | Required | Description | | ||
| ---------- | --------------- | -------- | ---------------------- | | ||
| `flags` | `Computable<T>` | `true` | All pre-computed flags | | ||
| `children` | `ReactNode` | `true` | React children | | ||
```tsx | ||
// index.tsx | ||
import { NullBackend } from "flag"; | ||
import { MyApplication } from "./app"; | ||
import { FlagsProvider, Flag } from "./flags"; | ||
import { FlagBackendProvider } from "./flags"; | ||
const backend = new NullBackend(); | ||
const instance = ( | ||
<FlagsProvider flags={flags}> | ||
<FlagBackendProvider backend={backend}> | ||
<MyApplication /> | ||
</FlagsProvider> | ||
</FlagBackendProvider> | ||
); | ||
@@ -158,67 +68,26 @@ | ||
### Flag | ||
### useFlag | ||
Returned as part of `createFlags()`. Renders a some UI based on whether a flag is truthy or falsy. It's a glorified if statement 😬. Must be used in side of `FlagsProvider`. | ||
_Returned as part of `createFlags<T>()`._ | ||
| Props | Type | Required | Description | | ||
| ------------------- | ----------------------------- | -------- | ----------------------------------- | | ||
| `name` | `string[]` | `true` | Must be a valid key path of `T` | | ||
| `children` | `ReactNode` | `false` | React children | | ||
| `render` | `(flags: T) => ReactNode` | `false` | Function that returns a `ReactNode` | | ||
| `fallbackRender` | `(flags: T) => ReactNode` | `false` | Function that returns a `ReactNode` | | ||
| `component` | `ComponentType<{ flags: T }>` | `false` | React Component with `T` as props | | ||
| `fallbackComponent` | `ComponentType<{ flags: T }>` | `false` | React Component with `T` as props | | ||
A hook to fetch a single flag. Requires a valid key path and a default value. The key path must terminate at a string, boolean or number and the default value must be of the same type that it terminates. Forcing a default to be provided will minimize the change of a runtime error occurring. | ||
Order of deciding which of these nodes to renders is as follows: | ||
| Args | Type | Required | Description | | ||
| -------------- | -------------------------------------------- | -------- | ---------------------------------------------------------- | | ||
| `keyPath` | `Types.KeyPath<T> \| Types.KeyPathString<T>` | `true` | A valid key path of `T` to a string, boolean or number | | ||
| `defaultValue` | `GetValueFromKeyPath<T, KP>` | `true` | A fallback in case it is not available in the `Backend<T>` | | ||
- If the flag is `truthy`: | ||
- render `children` if defined | ||
- call `render` with `T` if defined _or_ | ||
- call `component` with `{flags: T}` if defined _else_ | ||
- return `null` | ||
- If the flag is `falsy`: | ||
- call `fallbackRender` with `T` if defined _or_ | ||
- call `fallbackComponent` with `{ flags: T }` if defined _else_ | ||
- return `null` | ||
```tsx | ||
<Flag | ||
name={["features", "useMyCoolNewThing"]} | ||
render={() => <div>Rendered on truthy</div>} | ||
fallbackRender={() => <div>Rendered on falsy</div>} | ||
/> | ||
``` | ||
### useFlags | ||
Returned as part of `createFlags()`. A React hook that returns all of the flags. Must be used in side of `FlagsProvider`. | ||
```tsx | ||
// my-component.tsx | ||
import { useFlags } from "./flags"; | ||
import { useFlag } from "./flags"; | ||
const MyComponent = () => { | ||
const flags = useFlags(); | ||
/** | ||
* The key path can be either an array or string of keys joined by `.` | ||
* It _must_ terminate at a string, boolean or number type. | ||
*/ | ||
const apiUrl = useFlag(["config", "apiUrl"], "https://example.com"); | ||
const apiUrl2 = useFlag("config.apiUrl", "https://example.com"); | ||
return <div>The API url is "{flags.config.apiUrl}"</div>; | ||
}; | ||
``` | ||
### useFlag | ||
Returned as part of `createFlags()`. A React hook to return a single flag. Must be used in side of `FlagsProvider`. | ||
| Args | Type | Required | Description | | ||
| --------- | ---------- | -------- | ------------------------------- | | ||
| `keyPath` | `string[]` | `true` | Must be a valid key path of `T` | | ||
```tsx | ||
// my-component.tsx | ||
import { useFlags } from "./flags"; | ||
const MyComponent = () => { | ||
const apiUrl = useFlag(["config", "apiUrl"]); | ||
return <div>The API url is "{apiUrl}"</div>; | ||
@@ -228,94 +97,30 @@ }; | ||
## Redux API | ||
### Flag | ||
### createFlagsReducer | ||
_Returned as part of `createFlags<T>()`._ | ||
Returned as part of `createReduxBindings(...)`. Creates a reducer to be used in your Redux stores. | ||
Renders a some UI based on whether a flag is `false` or not. (It's a glorified if statement 😬). | ||
| Args | Type | Required | Description | | ||
| ------- | ---- | -------- | ------------------------------- | | ||
| `flags` | `T` | `true` | The initial value of your flags | | ||
| Args | Type | Required | Description | | ||
| -------------- | -------------------------------------------- | -------- | ---------------------------------------------------------- | | ||
| `keyPath` | `Types.KeyPath<T> \| Types.KeyPathString<T>` | `true` | A valid key path of `T` to a string, boolean or number | | ||
| `defaultValue` | `GetValueFromKeyPath<T, KP>` | `true` | A fallback in case it is not available in the `Backend<T>` | | ||
| `render` | `(flags: T) => ReactNode` | `true` | Function that returns a `ReactNode` | | ||
| `fallback` | `() => ReactNode` | `false` | Function that returns a `ReactNode` | | ||
```tsx | ||
// reducer.ts | ||
import { combineReducers } from "redux"; | ||
import { Computable } from "flag"; | ||
import { createFlagsReducer, MyFlags } from "./flags"; | ||
import { otherReducer } from "./other-reducer"; | ||
const flags: Computable<MyFlags> = { | ||
// ... | ||
}; | ||
export default combineReducers({ | ||
// 👇 must use the "flags" key of your state | ||
flags: createFlagsReducer(flags), | ||
other: otherReducer | ||
}); | ||
<Flag | ||
name="features.useMyCoolNewThing" | ||
defaultValue={false} | ||
render={() => <div>Rendered on truthy</div>} | ||
fallback={() => <div>Rendered on falsy</div>} | ||
/> | ||
``` | ||
### getFlagsSelector | ||
## Backends | ||
A selector to retrieve _computed_ flags from Redux state. It is not enough to say `state.flags` because `createFlagsReducer` does not eagerly evaluate computable flags. | ||
(Though I suppose if you don't use any computable flags, then you don't necessarily need this 🤷♂️.) | ||
## Setting NODE_ENV | ||
```tsx | ||
// reducer.ts | ||
import { getFlagsSelector, MyFlags } from "./flags"; | ||
type State = { | ||
flags: MyFlags; | ||
// ... | ||
}; | ||
// ... | ||
export const getFlags = (state: State) => getFlagsSelector(state); | ||
``` | ||
### ConnectedFlagsProvider | ||
Returned as part of `createReduxBindings(...)`. Wraps `FlagsProvider`, fetching the flags from Redux state. | ||
```tsx | ||
import { Provider } from "redux"; | ||
import { MyApplication } from "./app"; | ||
import { ConnectedFlagsProvider } from "./flags"; | ||
import { store } from "./store"; | ||
const instance = ( | ||
<Provider store={store}> | ||
<ConnectedFlagsProvider> | ||
<MyApplication /> | ||
</ConnectedFlagsProvider> | ||
</Provider> | ||
); | ||
React.render(instance, document.querySelector("#app")); | ||
``` | ||
### setFlagsAction | ||
Returned as part of `createReduxBindings(...)`. A dispatchable action that sets flags. Merges a partial value of pre-computed flags with existing pre-computed flags. | ||
| Args | Type | Required | Description | | ||
| ------- | --------------- | -------- | -------------------------- | | ||
| `flags` | `Computable<T>` | `true` | Partial pre-computed flags | | ||
```tsx | ||
import { Thunk } from "redux-thunk"; | ||
import { setFlagsAction } from "./flags"; | ||
export const someThunk: Thunk<any> = ({ dispatch }) => async () => { | ||
const user = await fetchUser(); | ||
dispatch(setFlagsAction(user.flags)); | ||
// ... | ||
}; | ||
``` | ||
## License | ||
MPL-2.0 |
@@ -1,5 +0,24 @@ | ||
import { createFlags } from "./create-flags"; | ||
import type { AsyncMutableRefObject } from "async-ref"; | ||
import type { KeyPath, KeyPathString, GetValueFromKeyPath, ExternalStore, GetValueFromKeyPathString } from "./types"; | ||
import type { Backend } from "./backends"; | ||
export { Computable } from "deep-computed"; | ||
export { createFlags }; | ||
export default createFlags; | ||
type KeyPath_<T> = KeyPath<T>; | ||
type KeyPathString_<T> = KeyPathString<T>; | ||
type GetValueFromKeyPath_<T, KP extends KeyPath<T>> = GetValueFromKeyPath<T, KP>; | ||
type GetValueFromKeyPathString_<T, KP extends KeyPathString<T>> = GetValueFromKeyPathString<T, KP>; | ||
type ExternalStore_<T> = ExternalStore<T>; | ||
type Backend_<T> = Backend<T>; | ||
type AsyncMutableRefObject_<T> = AsyncMutableRefObject<T>; | ||
export module Types { | ||
export type KeyPath<T> = KeyPath_<T>; | ||
export type KeyPathString<T> = KeyPathString_<T>; | ||
export type GetValueFromKeyPath<T, KP extends KeyPath<T>> = GetValueFromKeyPath_<T, KP>; | ||
export type GetValueFromKeyPathString<T, KP extends KeyPathString<T>> = GetValueFromKeyPathString_<T, KP>; | ||
export type ExternalStore<T> = ExternalStore_<T>; | ||
export type Backend<T> = Backend_<T>; | ||
export type AsyncMutableRefObject<T> = AsyncMutableRefObject_<T>; | ||
} | ||
export { createFlags } from "./create-flags"; | ||
export { AlwaysBackend, AbstractBackend, ComputedBackend, NullBackend, StaticBackend } from "./backends"; |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
Environment variable access
Supply chain riskPackage accesses environment variables, which may be a sign of credential stuffing or data theft.
Found 1 instance in 1 package
No v1
QualityPackage is not semver >=1. This means it is not stable and does not support ^ ranges.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
3
9
21
50317
780
2
124
4
1
+ Added@types/react@^18.0.5
+ Addedasync-ref@^0.1.6
+ Added@types/react@18.3.16(transitive)
+ Addedasync-ref@0.1.6(transitive)
+ Addedreact@18.3.1(transitive)
- Removeddeep-computed@^0.2.0
- Removedinvariant@^2.2.4
- Removedlodash@^4.17.11
- Removeduseful-types@^0.4.0
- Removed@babel/runtime@7.26.0(transitive)
- Removed@types/hoist-non-react-statics@3.3.6(transitive)
- Removed@types/react@16.14.62(transitive)
- Removed@types/react-dom@16.9.25(transitive)
- Removed@types/react-redux@7.1.34(transitive)
- Removed@types/scheduler@0.16.8(transitive)
- Removeddeep-computed@0.2.0(transitive)
- Removedhoist-non-react-statics@3.3.2(transitive)
- Removedinvariant@2.2.4(transitive)
- Removedlodash@4.17.21(transitive)
- Removedobject-assign@4.1.1(transitive)
- Removedprop-types@15.8.1(transitive)
- Removedreact@16.14.0(transitive)
- Removedreact-dom@16.14.0(transitive)
- Removedreact-is@16.13.117.0.2(transitive)
- Removedreact-redux@7.2.9(transitive)
- Removedredux@4.2.1(transitive)
- Removedregenerator-runtime@0.14.1(transitive)
- Removedscheduler@0.19.1(transitive)
- Removeduseful-types@0.4.0(transitive)