Skip to content

Knockout to React Migration

Thomas Belin edited this page Mar 1, 2022 · 11 revisions

Conventions

Turning Knockout components into React components

  • Params need to be unwrapped in the registerReactComponent template using ko.unwrap

  • 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

  • CSS should be moved into the React components where possible

  • Styles should be created via the css prop (not the style property)

  • CSS mixins should be moved to Util/CSSMixin

  • CSSMixins must return CSSObject

Writing tests for migrated React components

  • 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.

Example PRs / Best Practices

Skeleton React Component (TSX)

Preferred technique (registerStaticReactComponent).

The registerStaticReactComponent should be preferred when possible (instead of registerReactComponent). This method has better performance and will avoid many useless and costly full re-renders.

import React from 'react';
import {registerStaticReactComponent} from 'Util/ComponentUtil';

const Component = (props) => {}

registerStaticReactComponent('component-name', Component);

/* As opposed to:
registerReactComponent('component-name', {  
  component: CustomComponent,
  template: '<div data-bind="react: {prop1, prop2}"></div>',
}
*/

A Static component is a component that does not need props update from the knockout world. Say you have a component instantiation like this in ko templates

<component-name params='user: user'></component-name>

If the user observable is not going to emit anything during the lifetime of the component or if user is not an observable, you can safely use registerStaticReactComponent.

If in doubt, better use registerReactComponent and evaluate later if registerStaticReactComponent can actually be used.

Deprecated technique (still needed for components that need props update from knockout)

import React from 'react';
import {registerReactComponent} from 'Util/ComponentUtil';

interface CustomComponentProps {}

const CustomComponent: React.FC<CustomComponentProps> = (props: CustomComponentProps) => {
  return <div>Custom Component</div>;
}

export default CustomComponent;

registerReactComponent<CustomComponentProps>('custom-component', {
  component: CustomComponent,
  template: '<div data-bind="react: {}"></div>',
});

Examples

Refactor element as HTMLElement

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>();
  // ...
}

Refactor $(...)

Knockout

const inputElement = $(element).find('.search-input');

React

const inputElement = useRef<HTMLInputElement>();

return (
  <input className="search-input" ref={inputElement} />
);

Refactor ko.observable

Knockout

const isPlaying = ko.observable(false);

React

const [isPlaying, setIsPlaying] = useState<boolean>(false);

Refactor <!-- ko if -->

Knockout

<!-- ko if: isUploaded -->
  <div>Uploaded</div>
<!-- /ko -->

React

{ isUploaded && <div>Uploaded</div> }

Refactor <!-- ko foreach -->

Knockout

<!-- ko foreach: selectedUsers -->
  <span data-bind="text: name()"></span>
<!-- /ko -->

React

{selectedUsers.map(({name, id}) => (
  <span key={id}>{name}</span>
))}

Refactor addEventListener & removeEventListener

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]);

Refactor css class

Knockout

<div class="media-button media-button-play"></div>

React

<div className="media-button media-button-play"></div>

Refactor data-bind="css"

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,
  })}
/>

Refactor data-bind="click"

Knockout

<div data-bind="click: onClickPlay"></div>

React

<div onClick={onClickPlay}></div>

Refactor ko.components.register

Knockout

ko.components.register('legal-hold-dot', {
  template: `
    <div>
      <!-- ko if: isPending() -->
        <pending-icon></pending-icon>
      <!-- /ko -->
      <!-- ... -->
    </div>
    `,
  viewModel: function (params): void {
    // ...
  },
});

React

registerReactComponent('legal-hold-dot', {
  component: LegalHoldDot,
  template: '<div data-bind="react: {conversation, isPending: ko.unwrap(isPending), large, legalHoldModal}"></div>',
});

Refactor Params

  • Params will be renamed to Props
  • All parameters that are of type ko.Subscribable have to become plain properties
  • to make them plain properties, they must be unwrapped in the template definition when using registerReactComponent
  • Props have to be exported to be used with the TestPage 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;
}

...
registerReactComponent('componentname', {
  component: ...,

  template: `<div 
  data-bind="react: {
    asset, 
    cancel, 
    large, 
    pause, 
    play, 
    src, 
    transferState: ko.unwrap(transferState), 
    uploadProgress: ko.unwrap(uploadProgress)
  }"></div>`,

})

Handling subscribable child props

  • useKoSubscribableChildren takes root and names of child props to watch, updates if root changes
  • It subscribes to child value changes and always returns the latest value

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>
  )
}

Refactor icons

Knockout

<read-icon class="panel__action-item__icon"></read-icon>

React

import Icon from 'Components/Icon';

// ...

<Icon name="read-icon" className="panel__action-item__icon" />

// or

<Icon.Read className="panel__action-item__icon" />

Migrating SVGs

<svg
  viewBox="0 0 16 16"
  dangerouslySetInnerHTML={{__html: SVGProvider['mic-off-icon']?.documentElement?.innerHTML}}
></svg>

Common Errors

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.

Clone this wiki locally