Skip to content

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

Merged
merged 3 commits into from
Mar 15, 2020

Conversation

jamesopti
Copy link
Contributor

@jamesopti jamesopti commented Mar 11, 2020

Summary

Updates <OptimizelyFeature> to be a functional component that

Also 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 in useFeature 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.

@@ -107,14 +107,14 @@ describe('<OptimizelyFeature>', () => {

// while it's waiting for onReady()
expect(component.text()).toBe('')
resolver.resolve({ sucess: true })
resolver.resolve({ success: true })
Copy link
Contributor Author

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 🤦‍♂

Copy link
Contributor

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 => {
Copy link
Contributor Author

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?

Copy link
Contributor

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?

Copy link

@pauloptimizely pauloptimizely Mar 11, 2020

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

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Paul the wizard!

Copy link
Contributor Author

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.

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.

Copy link
Contributor Author

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 => {

Copy link
Contributor

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?

Copy link

@pauloptimizely pauloptimizely Mar 13, 2020

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

Copy link
Contributor Author

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.

Copy link
Contributor Author

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.

@circAssimilate circAssimilate self-requested a review March 11, 2020 22:09
Copy link
Contributor

@circAssimilate circAssimilate left a 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)
Copy link
Contributor

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.

Copy link
Contributor Author

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 });
Copy link
Contributor

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?

Copy link
Contributor Author

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 and attributes isnt
  • optimizely here is the ReactSDK instance, which specifically checks for each argument to be undefined.

As such, I think this is the best approach.

@jamesopti jamesopti force-pushed the james/feature_component_refactor branch 3 times, most recently from 22e08ba to 0f1e2f4 Compare March 13, 2020 19:27
@jamesopti jamesopti force-pushed the james/feature_component_refactor branch from 0f1e2f4 to 9a65698 Compare March 13, 2020 20:05
Copy link
Contributor

@circAssimilate circAssimilate left a 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!

@jamesopti jamesopti force-pushed the james/feature_component_refactor branch from 9a65698 to c57e44e Compare March 13, 2020 21:19
@@ -14,7 +14,7 @@
* limitations under the License.
*/
import { useCallback, useContext, useEffect, useState } from 'react';
import * as optimizely from '@optimizely/optimizely-sdk';
Copy link
Contributor Author

@jamesopti jamesopti Mar 13, 2020

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.

Copy link
Contributor

@mjc1283 mjc1283 Mar 16, 2020

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.

@jamesopti jamesopti force-pushed the james/feature_component_refactor branch from 9188c0b to 783ba81 Compare March 15, 2020 20:41
@jamesopti jamesopti merged commit 14a80f2 into master Mar 15, 2020
@jamesopti jamesopti deleted the james/feature_component_refactor branch March 15, 2020 22:15
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants