-
Notifications
You must be signed in to change notification settings - Fork 37
enhancement (<OptimizelyFeature>): Convert to functional component that uses useFeature hook #32
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
src/Feature.spec.tsx
Outdated
@@ -107,14 +107,14 @@ describe('<OptimizelyFeature>', () => { | |||
|
|||
// while it's waiting for onReady() | |||
expect(component.text()).toBe('') | |||
resolver.resolve({ sucess: true }) | |||
resolver.resolve({ success: true }) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Success was misspelled in this entire test file, so all the tests were actually running as though success was false
🤦♂
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
😱
src/Feature.tsx
Outdated
variables: {}, | ||
} | ||
} | ||
const FeatureComponent = (props: FeatureProps): any => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Im not sure what Typescript wants for the return type of this functional component.
I tried a couple things but nothing pleased the withOptimizely
definition of Component: React.ComponentType<P>,
@mjc1283 any ideas for how to improve this?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would it be React.FC
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
returning JSX.Element
instead of React.ReactNode
for the children
render-prop, did the trick for me locally.
export interface FeatureProps extends WithOptimizelyProps {
feature: string
timeout?: number
autoUpdate?: boolean
overrideUserId?: string
overrideAttributes?: UserAttributes
children: (isEnabled: boolean, variables: VariableValuesObject) => JSX.Element
}
const FeatureComponent: React.FC<FeatureProps> = (props: FeatureProps) => {
...
I'd have to do some research on the exact differences
Update: Stack overflow explaining the differences https://stackoverflow.com/questions/58123398/when-to-use-jsx-element-vs-reactnode-vs-reactelement
Update: the withOptimizely
HOC is expecting the Component arg to be of type React.ComponentType
https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/types/react/index.d.ts#L79 . So the function you supply it has be of FC
or SFC
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Paul the wizard!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
returning
JSX.Element
instead ofReact.ReactNode
for thechildren
render-prop, did the trick for me locally.
I see that works, but I think it's semantically incorrect. props.children
can return any valid react markup, for which ReactNode
is correct (Doesn't have to be JSX):
type ReactNode = ReactChild | ReactFragment | ReactPortal | boolean | null | undefined;
I confirmed that the issue is with the children
prop's return value, specifically.
Upon searching a little bit, it looks like I'm not the only one!
DefinitelyTyped/DefinitelyTyped#18051
The suggestion to wrap the return value in a fragment works (<>{children(...)}</>
), but to me feels incorrect/clunky, as it's technically modifying the react tree.
What should we do? I suppose returning the children inside a fragment isnt the worst, but I might prefer to tsignore this.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think my vote is to use any
to get around this and leave a note:
const FeatureComponent: React.FunctionComponent<FeatureProps> = (props): any => {
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@jamesopti Does the fragment workaround give better type safety than using any
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Using any
works for me for now, until this is fixed upstream here DefinitelyTyped/DefinitelyTyped#18051.
If we don't want to wait and prefer better type safety we could change the withOptimizely signature
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@jamesopti Does the fragment workaround give better type safety than using any?
It gives better type safety, but at the expense of changing the actual compiled code (to wrap everything in a fragment).
Considering all were doing is rendering children passed by the consumer, I feel like the type safety we gain is small (the consumers children
function returns valid react code).
But maybe that is useful? I guess injecting a Fragment into the rendered tree isnt so bad either.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I went with the Fragment. Decided its the slightly lesser of 2 evils here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Great refactor! Requesting changes to add a CHANGELOG. Also, doin't think a few more tests to make sure options and overrides are being passed would hurt.
Also, re: this, I think you determined we would major release after this - are you just mentioning that it won't be because of this item? Sgtm if so.
I dont think this should be considered a breaking change (and thus warrant a major release), but I wanted to point it out.
src/Feature.tsx
Outdated
return children(isEnabled, variables) | ||
} | ||
} | ||
return children(isEnabled, variables) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is there value in passing clientReady
and didTimeout
like useFeature
has? One usecase could be the decision to show a spinner while disabled until the client is ready then to show the default case.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I like this idea. Will add it in
src/Feature.tsx
Outdated
variables, | ||
clientReady, | ||
didTimeout | ||
] = useFeature(feature, { timeout, autoUpdate }, { overrideUserId, overrideAttributes }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should we add tests to make sure the options and overrides are passed through? Also, it seems alright to pass these even if they're not defined (looking at the tests above) but I think it could be better to construct an args array and not pass any values if they don't exist. Wdyt?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should we add tests to make sure the options and overrides are passed through?
Done!
Also, it seems alright to pass these even if they're not defined (looking at the tests above) but I think it could be better to construct an args array and not pass any values if they don't exist. Wdyt?
So 2 thoughts:
- Since these are positional args, you still need to account for the case where
userId
is not provided andattributes
isnt optimizely
here is theReactSDK
instance, which specifically checks for each argument to beundefined
.
As such, I think this is the best approach.
22e08ba
to
0f1e2f4
Compare
0f1e2f4
to
9a65698
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Great work! Thanks, James!
9a65698
to
c57e44e
Compare
@@ -14,7 +14,7 @@ | |||
* limitations under the License. | |||
*/ | |||
import { useCallback, useContext, useEffect, useState } from 'react'; | |||
import * as optimizely from '@optimizely/optimizely-sdk'; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this should have caused a lint error below (using the variable name optimizely when its already declared in outer scope)
@mjc1283 - Is the tsc compiler smart enough to figure out which usage is a type?
The scopes make it work as expected but I thought the tsc linter would complain.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
+1 to linting against this. In our example code we try to use optimizelySdk
as the name of the import.
9188c0b
to
783ba81
Compare
Summary
Updates
<OptimizelyFeature>
to be a functional component thatAlso adds support for userId and userAttribute overrides.
Addresses #30
Release
Is this a breaking change? In theory the logic should behave the same, as #28 was designed to implement the same logic as
<OptimizelyFeature>
.Timing wise, when
isServerSide
is false, the state setting of the correct values of the feature/variables inuseFeature
happens in a second chained thennable, rather than the first.I dont think this should be considered a breaking change (and thus warrant a major release), but I wanted to point it out.