import React, { createContext, FC, useContext, useEffect, useMemo, useRef, useState } from 'react' import Emitter from './utils/emitter' const events = new Emitter() import { IFlagsmith, IFlagsmithTrait, IFlagsmithFeature, IState } from './types' export const FlagsmithContext = createContext | null>(null) export type FlagsmithContextType = { flagsmith: IFlagsmith // The flagsmith instance options?: Parameters[0] // Initialisation options, if you do not provide this you will have to call init manually serverState?: IState children: React.ReactNode } export const FlagsmithProvider: FC = ({ flagsmith, options, serverState, children }) => { const firstRenderRef = useRef(true) if (flagsmith && !flagsmith?._trigger) { flagsmith._trigger = () => { // @ts-expect-error using internal function, consumers would never call this flagsmith?.log('React - trigger event received') events.emit('event') } } if (flagsmith && !flagsmith?._triggerLoadingState) { flagsmith._triggerLoadingState = () => { events.emit('loading_event') } } if (serverState && !flagsmith.initialised) { flagsmith.setState(serverState) } if (firstRenderRef.current) { firstRenderRef.current = false if (options) { flagsmith .init({ ...options, state: options.state || serverState, onChange: (...args) => { if (options.onChange) { options.onChange(...args) } }, }) .catch((error) => { // @ts-expect-error using internal function, consumers would never call this flagsmith?.log('React - Failed to initialize flagsmith', error) events.emit('event') }) } } return {children} } const useConstant = function (value: T): T { const ref = useRef(value) if (!ref.current) { ref.current = value } return ref.current } const flagsAsArray = (_flags: any): string[] => { if (typeof _flags === 'string') { return [_flags] } else if (typeof _flags === 'object') { // eslint-disable-next-line no-prototype-builtins if (_flags.hasOwnProperty('length')) { return _flags } } throw new Error('Flagsmith: please supply an array of strings or a single string of flag keys to useFlags') } const getRenderKey = (flagsmith: IFlagsmith, flags: string[], traits: string[] = []) => { return flags .map((k) => { return `${flagsmith.getValue(k)}${flagsmith.hasFeature(k)}` }) .concat(traits.map((t) => `${flagsmith.getTrait(t)}`)) .join(',') } export function useFlagsmithLoading() { const flagsmith = useContext(FlagsmithContext) const [loadingState, setLoadingState] = useState(flagsmith?.loadingState) useEffect(() => { if (!flagsmith) return setLoadingState(flagsmith.loadingState) const unsubscribe = events.on('loading_event', () => { setLoadingState(flagsmith.loadingState) }) return () => { unsubscribe() } }, [flagsmith]) return loadingState } type UseFlagsReturn, T extends string> = F extends string ? { [K in F]: IFlagsmithFeature } & { [K in T]: IFlagsmithTrait } : { [K in keyof F]: IFlagsmithFeature } & { [K in T]: IFlagsmithTrait } /** * Example usage: * * // A) Using string flags: * useFlags<"featureOne"|"featureTwo">(["featureOne", "featureTwo"]); * * // B) Using an object for F - this can be generated by our CLI: https://github.com/Flagsmith/flagsmith-cli : * interface MyFeatureInterface { * featureOne: string; * featureTwo: number; * } * useFlags(["featureOne", "featureTwo"]); */ export function useFlags, T extends string = string>( _flags: readonly (F | keyof F)[], _traits: readonly T[] = [] ) { const flags = useConstant(flagsAsArray(_flags)) const traits = useConstant(flagsAsArray(_traits)) const flagsmith = useContext(FlagsmithContext) const [renderRef, setRenderRef] = useState(getRenderKey(flagsmith as IFlagsmith, flags, traits)) useEffect(() => { if (!flagsmith) return setRenderRef(getRenderKey(flagsmith, flags, traits)) const unsubscribe = events.on('event', () => { setRenderRef((prev) => { const next = getRenderKey(flagsmith, flags, traits) if (prev === next) return prev // @ts-expect-error using internal function, consumers would never call this flagsmith?.log('React - useFlags flags and traits have changed') return next }) }) return () => { unsubscribe() } }, [flagsmith, flags, traits]) const res = useMemo(() => { const res: any = {} flags .map((k) => { res[k] = { enabled: flagsmith!.hasFeature(k), value: flagsmith!.getValue(k), } }) .concat( traits?.map((v) => { res[v] = flagsmith!.getTrait(v) }) ) return res }, [renderRef]) return res as UseFlagsReturn } export function useFlagsmith, T extends string = string>() { const context = useContext(FlagsmithContext) if (!context) { throw new Error('useFlagsmith must be used with in a FlagsmithProvider') } return context as unknown as IFlagsmith }