Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .prettierrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@
"proseWrap": "always",
"singleQuote": true,
"printWidth": 120,
"trailingComma": "all",
"tabWidth": 4,
"semi": false,
"overrides": [
{
"files": "*.md",
Expand Down
4 changes: 4 additions & 0 deletions flagsmith-core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -829,6 +829,10 @@ const Flagsmith = class {
options.headers['Flagsmith-Application-Version'] = this.applicationMetadata.version;
}

if (SDK_VERSION) {
options.headers['Flagsmith-SDK-user-agent'] = `flagsmith-js-sdk/${SDK_VERSION}`
}

if (headers) {
Object.assign(options.headers, headers);
}
Expand Down
139 changes: 60 additions & 79 deletions react.tsx
Original file line number Diff line number Diff line change
@@ -1,41 +1,30 @@
import React, {
createContext,
FC,
useCallback,
useContext,
useEffect,
useMemo,
useRef,
useState,
} from 'react';
import Emitter from './utils/emitter';
const events = new Emitter();
import React, { createContext, FC, useCallback, 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<IFlagsmith<string,string> | null>(null)
export const FlagsmithContext = createContext<IFlagsmith<string, string> | null>(null)
export type FlagsmithContextType = {
flagsmith: IFlagsmith // The flagsmith instance
options?: Parameters<IFlagsmith['init']>[0] // Initialisation options, if you do not provide this you will have to call init manually
serverState?: IState
children: React.ReactNode;
children: React.ReactNode
}

export const FlagsmithProvider: FC<FlagsmithContextType> = ({
flagsmith, options, serverState, children,
}) => {
export const FlagsmithProvider: FC<FlagsmithContextType> = ({ 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');
flagsmith?.log('React - trigger event received')
events.emit('event')
}
}

if (flagsmith && !flagsmith?._triggerLoadingState) {
flagsmith._triggerLoadingState = () => {
events.emit('loading_event');
events.emit('loading_event')
}
}

Expand All @@ -46,22 +35,24 @@ export const FlagsmithProvider: FC<FlagsmithContextType> = ({
if (firstRenderRef.current) {
firstRenderRef.current = false
if (options) {
flagsmith.init({
...options,
state: options.state || serverState,
onChange: (...args) => {
if (options.onChange) {
options.onChange(...args)
}
},
})
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 (
<FlagsmithContext.Provider value={flagsmith}>
{children}
</FlagsmithContext.Provider>
)
return <FlagsmithContext.Provider value={flagsmith}>{children}</FlagsmithContext.Provider>
}

const useConstant = function <T>(value: T): T {
Expand All @@ -72,7 +63,6 @@ const useConstant = function <T>(value: T): T {
return ref.current
}


const flagsAsArray = (_flags: any): string[] => {
if (typeof _flags === 'string') {
return [_flags]
Expand All @@ -82,29 +72,26 @@ const flagsAsArray = (_flags: any): string[] => {
return _flags
}
}
throw new Error(
'Flagsmith: please supply an array of strings or a single string of flag keys to useFlags',
)
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)}`
)))
})
.concat(traits.map((t) => `${flagsmith.getTrait(t)}`))
.join(',')
}

export function useFlagsmithLoading() {
const flagsmith = useContext(FlagsmithContext);
const [loadingState, setLoadingState] = useState(flagsmith?.loadingState);
const [subscribed, setSubscribed] = useState(false);
const flagsmith = useContext(FlagsmithContext)
const [loadingState, setLoadingState] = useState(flagsmith?.loadingState)
const [subscribed, setSubscribed] = useState(false)
const refSubscribed = useRef(subscribed)

const eventListener = useCallback(() => {
setLoadingState(flagsmith?.loadingState);
setLoadingState(flagsmith?.loadingState)
}, [flagsmith])
if (!refSubscribed.current) {
events.on('loading_event', eventListener)
Expand All @@ -120,26 +107,23 @@ export function useFlagsmithLoading() {
if (subscribed) {
events.off('loading_event', eventListener)
}
};
}
}, [flagsmith, subscribed, eventListener])

return loadingState
}

type UseFlagsReturn<
F extends string | Record<string, any>,
T extends string
> = [F] extends [string]
type UseFlagsReturn<F extends string | Record<string, any>, T extends string> = F extends string
? {
[K in F]: IFlagsmithFeature;
} & {
[K in T]: IFlagsmithTrait;
}
[K in F]: IFlagsmithFeature
} & {
[K in T]: IFlagsmithTrait
}
: {
[K in keyof F]: IFlagsmithFeature<F[K]>;
} & {
[K in T]: IFlagsmithTrait;
};
[K in keyof F]: IFlagsmithFeature<F[K]>
} & {
[K in T]: IFlagsmithTrait
}

/**
* Example usage:
Expand All @@ -154,61 +138,58 @@ type UseFlagsReturn<
* }
* useFlags<MyFeatureInterface>(["featureOne", "featureTwo"]);
*/
export function useFlags<
F extends string | Record<string, any>,
T extends string = string
>(
_flags: readonly (F | keyof F)[], _traits: readonly T[] = []
){
export function useFlags<F extends string | Record<string, any>, T extends string = string>(
_flags: readonly (F | keyof F)[],
_traits: readonly T[] = []
) {
const firstRender = useRef(true)
const flags = useConstant<string[]>(flagsAsArray(_flags))
const traits = useConstant<string[]>(flagsAsArray(_traits))
const flagsmith = useContext(FlagsmithContext)
const [renderRef, setRenderRef] = useState(getRenderKey(flagsmith as IFlagsmith, flags, traits));
const [renderRef, setRenderRef] = useState(getRenderKey(flagsmith as IFlagsmith, flags, traits))
const eventListener = useCallback(() => {
const newRenderKey = getRenderKey(flagsmith as IFlagsmith, flags, traits)
if (newRenderKey !== renderRef) {
// @ts-expect-error using internal function, consumers would never call this
flagsmith?.log("React - useFlags flags and traits have changed")
flagsmith?.log('React - useFlags flags and traits have changed')
setRenderRef(newRenderKey)
}
}, [renderRef])
const emitterRef = useRef(events.once('event', eventListener));


const emitterRef = useRef(events.once('event', eventListener))

if (firstRender.current) {
firstRender.current = false;
firstRender.current = false
// @ts-expect-error using internal function, consumers would never call this
flagsmith?.log("React - Initialising event listeners")
flagsmith?.log('React - Initialising event listeners')
}

useEffect(()=>{
useEffect(() => {
return () => {
emitterRef.current?.()
}
}, [])

const res = useMemo(() => {
const res: any = {}
flags.map((k) => {
flags
.map((k) => {
res[k] = {
enabled: flagsmith!.hasFeature(k),
value: flagsmith!.getValue(k),
}
}).concat(traits?.map((v) => {
})
.concat(
traits?.map((v) => {
res[v] = flagsmith!.getTrait(v)
}))
})
)
return res
}, [renderRef])

return res as UseFlagsReturn<F, T>
}

export function useFlagsmith<
F extends string | Record<string, any>,
T extends string = string
>() {
export function useFlagsmith<F extends string | Record<string, any>, T extends string = string>() {
const context = useContext(FlagsmithContext)

if (!context) {
Expand Down
1 change: 1 addition & 0 deletions test/init.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -292,6 +292,7 @@ describe('Flagsmith.init', () => {
headers: expect.objectContaining({
'Flagsmith-Application-Name': 'Test App',
'Flagsmith-Application-Version': '1.2.3',
'Flagsmith-SDK-user-agent': `flagsmith-js-sdk/${SDK_VERSION}`,
}),
}),
);
Expand Down
Loading