-
Notifications
You must be signed in to change notification settings - Fork 2.9k
Foundation: State Components & Hooks #8931
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
… have state components.
| // 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); |
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.
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); |
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.
@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.
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.
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); |
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 love how much simpler this is with the elimination of state class components and addition of hooks and useContext!
packages/experiments/src/components/PersonaCoin/examples/PersonaCoin.Example.tsx
Outdated
Show resolved
Hide resolved
Markionium
left a comment
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.
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); |
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.
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( |
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.
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.
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'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.
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.
Like this would be a useExpandMenu hook that could be swapped around whenever we want a menu to get expanded.
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 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.
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 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); | ||
| }, []); |
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.
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.
|
Ongoing investigation The failed build state of this PR is due to multiple versions of Opened #8983 to fix the CI issue. |
|
Component perf results:
|
|
There's going to be more churn on this PR. I disabled |
| if (!disabled) { | ||
| // Only update the state if the user hasn't provided it. | ||
| this.setState({ checked: !checked }); | ||
| const _onClick = useCallback( |
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.
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.)
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 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.
Pull request checklist
$ npm run changeDescription 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: AReact.useStatewrapper 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
Microsoft Reviewers: Open in CodeFlow