Created
May 31, 2022 22:01
-
-
Save Evanion/bd8f5618503357dd6cafdf27cc665836 to your computer and use it in GitHub Desktop.
WIP: A React feature toggle system.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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, | |
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | |
} | |
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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} />; | |
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | |
} | |
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> | |
]; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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