Skip to content

Instantly share code, notes, and snippets.

@Evanion
Created May 31, 2022 22:01
Show Gist options
  • Save Evanion/bd8f5618503357dd6cafdf27cc665836 to your computer and use it in GitHub Desktop.
Save Evanion/bd8f5618503357dd6cafdf27cc665836 to your computer and use it in GitHub Desktop.
WIP: A React feature toggle system.
import { createContext } from 'react';
import { ActionProps, FeatureConfig, FeatureState } from './feature.types';
const dispatch = <Feature extends string | number>(
value: ActionProps<Feature>
) => {};
export const createFeatureContext = <Feature extends string | number>(
features: FeatureState<Feature>,
config: FeatureConfig
) =>
createContext({
state: { features, config },
dispatch,
});
import { FeatureAction, ActionProps, FullFeatureState } from './feature.types';
export const reducer = <Feature extends string | number>(
state: FullFeatureState<Feature>,
action: ActionProps<Feature>
) => {
switch (action.action) {
case FeatureAction.Activate:
return {
...state,
features: {
...state.features,
[action.feature]: { ...state.features[action.feature], active: true },
},
};
case FeatureAction.Deactivate:
return {
...state,
features: {
...state.features,
[action.feature]: {
...state.features[action.feature],
active: false,
},
},
};
default:
return state;
}
};
import * as React from 'react';
import { useFeatureProvider } from '.';
import { FeatureConfig, FeatureContext, FeatureState } from './feature.types';
interface Props<Feature extends string | number> {
features: FeatureState<Feature>;
config: FeatureConfig;
children: React.ReactNode;
context: FeatureContext<Feature>;
}
export const FeatureProvider = <Feature extends string | number>({
features,
config,
children,
context: FeatureContext,
}: Props<Feature>) => {
const contextValue = useFeatureProvider({ features, config });
return <FeatureContext.Provider value={contextValue} children={children} />;
};
import { FeatureAction, ActionProps, FullFeatureState } from './feature.types';
export const reducer = <Feature extends string | number>(
state: FullFeatureState<Feature>,
action: ActionProps<Feature>
) => {
switch (action.action) {
case FeatureAction.Activate:
return {
...state,
features: {
...state.features,
[action.feature]: { ...state.features[action.feature], active: true },
},
};
case FeatureAction.Deactivate:
return {
...state,
features: {
...state.features,
[action.feature]: {
...state.features[action.feature],
active: false,
},
},
};
default:
return state;
}
};
export enum FeatureAction {
Activate = 'feature.action.activate',
Deactivate = 'feature.action.deactivate',
}
export type FeatureContext<Feature extends string | number> = React.Context<{
state: FullFeatureState<Feature>;
dispatch: React.Dispatch<ActionProps<Feature>>;
}>;
export type FeatureState<Feature extends string | number> = Record<
Feature,
FeatureOptions<Feature>
>;
export type FullFeatureState<Feature extends string | number> = {
features: FeatureState<Feature>;
config: FeatureConfig;
};
export interface ActionProps<Feature extends string | number> {
action: FeatureAction;
feature: Feature;
}
export interface FeatureConfig {
permissive: boolean;
}
export interface FeatureOptions<Feature extends string | number> {
active: boolean;
description?: string;
dependencies: Feature[];
isDependency?: boolean;
disabledByDependencies?: boolean;
configuredActive?: boolean;
}
export type FeatureTuple<Feature extends string | number> = [
Feature,
FeatureOptions<Feature>
];
import { FeatureOptions, FeatureState, FeatureTuple } from './feature.types';
/**
* @description Resolves and activates any features that are depended on by other features
*/
export function getPermissive<Feature extends string | number>(
state: FeatureState<Feature>
) {
return Object.values(state)
.filter((config) => config.active && config.dependencies.length)
.map((config: FeatureOptions<Feature>) => config.dependencies)
.flat(1)
.reduce((prev, current) => {
prev[current] = { ...state[current], active: true, isDependency: true };
return prev;
}, {} as FeatureState<Feature>);
}
/**
* @description Resolves and disables any features with disabled dependencies
*/
export function getRestrictive<Feature extends string | number>(
state: FeatureState<Feature>
) {
console.log('calling getRestrictive');
const featureKeyArr = Object.keys(state) as Feature[];
const featureArr = featureKeyArr.map(
(key) => [key, state[key]] as FeatureTuple<Feature>
);
return featureArr
.filter(([_key, feature]) => feature.active && feature.dependencies.length)
.reduce((acc, [key, feature]) => {
const originalActive = feature.active;
feature.active = feature.dependencies.reduce((previous, current) => {
if (!previous) return previous;
return state[current].active;
}, feature.active);
feature.disabledByDependencies = !feature.active && originalActive;
acc[key] = feature;
return acc;
}, {} as FeatureState<Feature>);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment