Skip to content

Conversation

@JasonGore
Copy link
Member

@JasonGore JasonGore commented May 2, 2019

Pull request checklist

Description of changes

This PR converts Foundation's state component type from classes to transform functions. This paves the way for these transform functions to use hooks.

Components that have state and use Foundation also needed to be converted as part of this change. In these cases their state components end up being one large hook used by Foundation.

In addition, some utility helpers have been added that can be used by components:

  • useControlledState: A React.useState wrapper that gives priority to controlled props.
  • getControlledDerivedProps: A simple helper that filters derived prop values while giving priority to controlled props.

Focus areas to test

  • Need to add hook unit tests as part of this PR.
  • Update create-component script.
Microsoft Reviewers: Open in CodeFlow

// Return array from here including list of controlled props?
// List of controlled props as createComponent option?
// updateViewProps functional arg that takes in partial view props and optional controlled prop list?
viewProps.text = getControlledDerivedProps(viewProps, 'text', viewProps.checked ? viewProps.onText : viewProps.offText);
Copy link
Member Author

Choose a reason for hiding this comment

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

Lines 32-48 are lines I'm still noodling on. I'm not sure if additional helpers will hurt or hinder here.. but I also think it's important to provide tools that make sure people respect controlled props in a consistent and standardized way.

export const usePersonaCoinState: IPersonaCoinComponent['state'] = props => {
// TODO: isPictureLoaded was controlled, does it need to be? it's not exposed through component props...
// For now use useState.
const [isPictureLoaded, setIsPictureLoaded] = useState(false);
Copy link
Member Author

Choose a reason for hiding this comment

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

@Markionium Did you intend isPictureLoaded to be a controlled prop? Since it's not part of IPersonaCoinProps it couldn't actually be controlled without users generating a type error.

Copy link
Member

Choose a reason for hiding this comment

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

Yes that seems correct afaik.

Persona uses the Image component and we want to show the initials before showing you the picture. It might not make much sense to allow the user of the component determine some custom logic for how that works.

If we'll need to do that at some point in the future thats probably fine, but for now i think it's ok to keep this controlled.

The user can listen to updates in the loading state by using the onPhotoLoadingStateChange callback.

_defaultStyles: styles
} as TViewProps & IDefaultSlotProps<any>;

return component.view(viewProps);
Copy link
Member Author

@JasonGore JasonGore May 2, 2019

Choose a reason for hiding this comment

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

I love how much simpler this is with the elimination of state class components and addition of hooks and useContext!

@JasonGore JasonGore requested a review from kenotron May 2, 2019 22:56
Copy link
Member

@Markionium Markionium left a comment

Choose a reason for hiding this comment

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

This is cool @JasonGore

export const usePersonaCoinState: IPersonaCoinComponent['state'] = props => {
// TODO: isPictureLoaded was controlled, does it need to be? it's not exposed through component props...
// For now use useState.
const [isPictureLoaded, setIsPictureLoaded] = useState(false);
Copy link
Member

Choose a reason for hiding this comment

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

Yes that seems correct afaik.

Persona uses the Image component and we want to show the initials before showing you the picture. It might not make much sense to allow the user of the component determine some custom logic for how that works.

If we'll need to do that at some point in the future thats probably fine, but for now i think it's ok to keep this controlled.

The user can listen to updates in the loading state by using the onPhotoLoadingStateChange callback.


const { disabled, onClick } = props;

const _onClick = useCallback(
Copy link
Contributor

Choose a reason for hiding this comment

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

One thing I wonder, would it be worthwhile to put this into their own state hook as well? Mainly to help set the precedent that we want generic helper functions that can act as building blocks.

Copy link
Member Author

Choose a reason for hiding this comment

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

I'd thought about it, but it seems behavior is so varied that the helpers would be so basic (or the options so complex) that I wonder if there is a net positive in having them. For example, do you always call callbacks? Do you call callbacks only if not processed? Do you stop propagation? Etc.

Copy link
Contributor

Choose a reason for hiding this comment

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

Like this would be a useExpandMenu hook that could be swapped around whenever we want a menu to get expanded.

Copy link
Contributor

Choose a reason for hiding this comment

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

I just feel like if we want there to be lots of shareable code between components, we should start trying now to set the standard that we want things to be in a common area. An example of where I would want this is for combobox, it should behave similarly IMO.

Copy link
Member Author

@JasonGore JasonGore May 3, 2019

Choose a reason for hiding this comment

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

I agree and I've done that where I think it makes sense, like useControlledState. Here, though, we'd have to pass disabled, onClick, expanded, setExpanded, and setMenuTarget all to a helper. I don't think making this a helper is quite so black and white in this case. There is a balance between reuse and simplicity and I'm not sure making this common would add much value. The logic is highly customized to this component's use case while being relatively simple. Most of the implementation is primarily interaction with surface area (the inputs I mentioned above) which isn't meaningfully reduced by extracting it out as a separate function.


const _onMenuDismiss = useCallback(() => {
setExpanded(false);
}, []);
Copy link
Member Author

@JasonGore JasonGore May 3, 2019

Choose a reason for hiding this comment

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

General note due to some questions (and me forgetting temporarily): useCallback is used here to memoize the callback and prevent the callback's reference from changing on every render. React documentation states that setState functions are 'stable' and therefore do not need to appear in deps array. The effect of an empty array is that only one version of the callback will ever be memoized.

@JasonGore
Copy link
Member Author

JasonGore commented May 3, 2019

Ongoing investigation

The failed build state of this PR is due to multiple versions of react and react-dom being present across packages (16.8.6 in perf-test, and 16.8.2 in most of the other packages) which causes an Invalid Hook Warning in perf-test when it triggers a hook inside of OUFR (via Stack.)

Opened #8983 to fix the CI issue.

@msft-github-bot
Copy link
Contributor

msft-github-bot commented May 6, 2019

Component perf results:

Scenario Target branch avg total (ms) PR avg total (ms) Target branch avg per item (ms) PR avg per item (ms) Is significant change Is regression
PrimaryButton 90.652 94.336 0.907 0.943 false false
BaseButton 36.603 37.733 0.366 0.377 false false
NewButton 120.422 125.060 1.204 1.251 true true
button 6.262 6.470 0.063 0.065 false false
DetailsRows without styles 204.024 199.980 2.040 2.000 false false
DetailsRows 226.335 226.141 2.263 2.261 false false
Toggles 57.818 56.899 0.578 0.569 false false
NewToggle 75.228 71.647 0.752 0.716 false false
DocumentCardTitle with truncation 29.198 28.926 0.292 0.289 false false

@JasonGore
Copy link
Member Author

There's going to be more churn on this PR. I disabled a11y-tests to confirm that deployed sites are broken due to hardcoded React versions in index.html files. I'll fix all of the index.html files and confirm the deployed sites are working before re-enabling the a11y-tests as part of this PR.

@JasonGore JasonGore requested a review from jordandrako as a code owner May 7, 2019 23:14
if (!disabled) {
// Only update the state if the user hasn't provided it.
this.setState({ checked: !checked });
const _onClick = useCallback(
Copy link
Member

Choose a reason for hiding this comment

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

still wrapping my brain on this... it seems that the function will be re-created on every render (but the useCallback will return the original value, I guess? for immutability purposes.)

Copy link
Member Author

Choose a reason for hiding this comment

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

I was wondering if we could use some kind of closure wrapper that encapsulates new function creation, passes in the dep values and only gets called when memoize retrieval fails. However, React states that there is no guarantee that previously memoized values will remain available, so we'd need some kind of hook to know when React is creating a new copy.

@dzearing dzearing merged commit 5f4051f into microsoft:fabric-7 May 8, 2019
@microsoft microsoft locked as resolved and limited conversation to collaborators Aug 30, 2019
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants