-
Notifications
You must be signed in to change notification settings - Fork 292
Knockout to React Migration
Benny Neugebauer edited this page Aug 23, 2021
·
11 revisions
- Optional knockout params must be specified in
registerReactComponent
optionalParams
- Params need to be unwrapped in the
registerReactComponent
template
- Params should be unwrapped with
ko.unwrap
- CSS should be moved into the React components where possible
- Styles should be created via the
css
prop (not thestyle
property) - CSS mixins should be moved to
Util/CSSMixin
-
CSSMixin
s must returnCSSObject
- Components should be
default
exported - Component prop interfaces should be named
${ComponentName}Props
- Component prop interfaces should be exported
- Every migrated component must have tests
- Every component should have a test with the name
Component.test.tsx
next to the component (i.e. LegalHoldDot.tsx & LegalHoldDot.test.tsx) - Every test extends a
TestPage
to create utility methods to access your component (i.e. finding it, clicking it, reading its values, etc.) - Adding a
data-uie-name
attribute (should be unique to your component's name!) to your component and accessing it via a CSS selector (i.e.this.get('[data-uie-name="legal-hold-dot-pending-icon"]')
) is the easiest way to "find" your component in a test - A test page is typed with the properties of your component (that's why you always should export
Props
of your React component), so that it can instantiate your component with different props for a test run - Basic tests include rendering your component with different props and accessing (through the extended
TestPage
's util methods) the component afterwards to check that the props are properly rendered - Advanced tests simulate events (i.e.
videoElement.dispatchEvent(new Event('pause'));
) to assert state updates of a component - The test should aim to test behavior not implementation of the component
- The test should not share state with other testcases. Favor functions that create a state over reusing existing state.
- Migrate LegalHoldDot to React (simple "leaf" component)
- Migrate SeekBar to React (component with event listeners)
-
Migrate TopPeople to React (component with
useKoSubscribable
) -
Creating a test that uses a feature flag (mimic
Config.getConfig().FEATURE.ENABLE_FEDERATION
)
import React from 'react';
import {registerReactComponent} from 'Util/ComponentUtil';
interface UserInputProps {
}
const UserInput: React.FC<UserInputProps> = (props: UserInputProps) => {
return (<></>);
}
export default UserInput;
registerReactComponent<UserInputProps>('user-input', {
component: UserInput,
// optionalParams: [],
template: '<div data-bind="react: {}"></div>',
});
Knockout
viewModel: {
createViewModel(params: MediaButtonProps, {element}: ko.components.ComponentInfo): MediaButtonComponent {
return new MediaButtonComponent(params, element as HTMLElement);
}
}
React
const MediaButton: React.FC<MediaButtonProps> = (props: MediaButtonProps) => {
const element = useRef<HTMLElement>();
// ...
}
Knockout
const inputElement = $(element).find('.search-input');
React
const inputElement = useRef<HTMLInputElement>();
return (
<input className="search-input" ref={inputElement} />
);
Knockout
const isPlaying = ko.observable(false);
React
const [isPlaying, setIsPlaying] = useState<boolean>(false);
Knockout
<!-- ko if: isUploaded() -->
<div>Uploaded</div>
<!-- /ko -->
React
{ isUploaded() && <div>Uploaded</div> }
Knockout
<!-- ko foreach: selectedUsers -->
<span data-bind="text: name()"></span>
<!-- /ko -->
React
{selectedUsers.map(({name}, index) => (
<span key={index}>${name}</span>
))}
Knockout
constructor() {
// ...
this.mediaElement.addEventListener('playing', this.onPlay);
this.mediaElement.addEventListener('pause', this.onPause);
}
readonly dispose = () => {
this.mediaElement.removeEventListener('playing', this.onPlay);
this.mediaElement.removeEventListener('pause', this.onPause);
};
React
useEffect(() => {
mediaElement.addEventListener('playing', onPlay);
mediaElement.addEventListener('pause', onPause);
return () => {
mediaElement.removeEventListener('playing', onPlay);
mediaElement.removeEventListener('pause', onPause);
};
}, [mediaElement]);
Knockout
<div class="media-button media-button-play"></div>
React
<div className="media-button media-button-play"></div>
Knockout
<div class="legal-hold-dot" data-bind="css: {'legal-hold-dot--active': !isPending(), 'legal-hold-dot--interactive': isInteractive, 'legal-hold-dot--large': large}"></div>
React
import cx from 'classnames';
// ...
<div
className={cx('legal-hold-dot', {
'legal-hold-dot--active': !isPending,
'legal-hold-dot--interactive': isInteractive,
'legal-hold-dot--large': large,
})}
/>
Knockout
<div data-bind="click: onClickPlay"></div>
React
<div onClick={onClickPlay}></div>
Knockout
ko.components.register('legal-hold-dot', {
template: `
<div>
<!-- ko if: isPending() -->
<pending-icon></pending-icon>
<!-- /ko -->
<!-- ... -->
</div>
`,
viewModel: function (params): void {
// ...
},
});
React
registerReactComponent<LegalHoldDotProps>('legal-hold-dot', {
component: LegalHoldDot,
optionalParams: ['conversation', 'isPending', 'large', 'legalHoldModal'],
template: '<div data-bind="react: {conversation, isPending: ko.unwrap(isPending), large, legalHoldModal}"></div>',
});
-
Params
will be renamed toProps
- All parameters that have a value of
ko.PureComputed
orko.observable
will become plain properties - Properties, which become plain properties, must be unwrapped in the
template
definition when usingregisterReactComponent
-
Props
have to be exported to be used with theTestPage
class
Knockout
interface Params {
asset: FileAsset;
cancel?: () => void;
large: boolean;
pause?: () => void;
play?: () => void;
src: HTMLMediaElement;
transferState: ko.PureComputed<AssetTransferState>;
uploadProgress: ko.PureComputed<number>;
}
React
interface MediaButtonProps {
asset: FileAsset;
cancel?: () => void;
large: boolean;
pause?: () => void;
play: () => void;
src: HTMLMediaElement;
transferState: AssetTransferState;
uploadProgress: number;
}
-
useKoSubscribable
always returns current value -
useKoSubscribableCallback
always calls callback function with current value
Knockout
<div data-bind="text: user.name()"></div>
React
import {useKoSubscribable} from 'Util/ComponentUtil';
const Comp = ({user}) => {
const name = useKoSubscribable(user.name, 'defaultName');
return <div>{name}</div>
}
import {useKoSubscribableCallback} from 'Util/ComponentUtil';
const Comp = ({user}) => {
const [bigName, setBigName] = useState('');
const name = useKoSubscribableCallback(user.name, name => setBigName(name.toUpperCase()));
return <div>{bigName}</div>
}
-
useKoSubscribable
does not update subscription if "root"-prop changes - e.g.
useKoSubscribable(user.name)
does not update ifuser
changes, subscription stays on the originaluser
-
useKoSubscribableChildren
takes root and names of child props to watch, updates if root changes
Knockout
<div>
<div data-bind="text: user.name()"></div>
<div data-bind="text: user.phone()"></div>
</div>
React
import {useKoSubscribableChildren} from 'Util/ComponentUtil';
const Comp = ({user}) => {
const {name, phone} = useKoSubscribableChildren(user, ['name', 'phone']);
return (
<div>
<div>{name}</div>
<div>{phone}</div>
</div>
)
}
Knockout
<read-icon class="panel__action-item__icon"></read-icon>
React
import NamedIcon from './NamedIcon';
// ...
<NamedIcon width={16} height={12} name="read-icon" />
<svg
viewBox="0 0 16 16"
dangerouslySetInnerHTML={{__html: SVGProvider['mic-off-icon']?.documentElement?.innerHTML}}
></svg>
Invalid value for prop `data-uie-value` on <div> tag. Either remove it from the element, or pass a string or number value to keep it in the DOM.
Problem:
<div data-uie-value={user.name}></div>
In the above case, the name
property was undefined
and not always guaranteed which was a problem because it is expected to be a string
.