Migrating React JS Lifecycle method to Hooks

153 views Asked by At

I'm trying to convert componentDidUpdate to useEffects. The examples I've seen were pretty straight forward but since the componentDidUpdate didn't seem too involved. I need make conditional changes but not sure what I really need to do to accomplish using conditions on the prevProps vs props

Here's the current code that I want to convert so that I can move to functional components away from classes. I added the comment //This is what I need to convert where I'm stuck.

import React, { useState, useEffect } from 'react'
import PropTypes from 'prop-types'
// import './ListingControl.css'
import { cssClasses, isDurationSame } from './util'
import { durationLike, isPlayerState } from './props'
import moment from 'moment'
import {
  SKIP_BACKWARD,
  FAST_BACKWARD,
  FORWARD,
  BACKWARD,
  FAST_FORWARD,
  SKIP_FORWARD,
  PAUSE,
  STOP,
  STEP_FORWARD,
  STEP_BACKWARD,
  DEFAULT_INTERVALS,
  DEFAULT_LENGTHS,
} from './constants'
import listingManager from './player'

const SYMBOLS = {
  [SKIP_BACKWARD]: '\u23ee',
  [FAST_BACKWARD]: '\u23ea',
  [FORWARD]: '\u23f4',
  [BACKWARD]: '\u23f4',
  [FAST_FORWARD]: '\u23e9',
  [SKIP_FORWARD]: '\u23ed',
  [PAUSE]: '\u23f8',
  [STOP]: '\u23f9',
  [STEP_FORWARD]: '\u23e9',
  [STEP_BACKWARD]: '\u23ea',
}

const SkipBackward = () => SYMBOLS[SKIP_BACKWARD]
const FastBackward = () => SYMBOLS[FAST_BACKWARD]
const Forward = () => SYMBOLS[FORWARD]
const Backward = () => (
  <div style={{ transform: 'scale(-1,1)' }}>{SYMBOLS[BACKWARD]}</div>
)
const FastForward = () => SYMBOLS[FAST_FORWARD]
const SkipForward = () => SYMBOLS[SKIP_FORWARD]
const Pause = () => SYMBOLS[PAUSE]
const Stop = () => SYMBOLS[STOP]
const StepForward = () => SYMBOLS[STEP_FORWARD]
const StepBackward = () => SYMBOLS[STEP_BACKWARD]

const NAME_MAPPING = {
  [SKIP_BACKWARD]: 'SkipBackward',
  [FAST_BACKWARD]: 'FastBackward',
  [BACKWARD]: 'Backward',
  [FORWARD]: 'Forward',
  [FAST_FORWARD]: 'FastForward',
  [SKIP_FORWARD]: 'SkipForward',
  [PAUSE]: 'Pause',
  [STOP]: 'Stop',
  [STEP_FORWARD]: 'StepForward',
  [STEP_BACKWARD]: 'StepBackward',
}

const CSS_CLASS_MAPPING = {
  [SKIP_BACKWARD]: 'skip-backward',
  [FAST_BACKWARD]: 'fast-backward',
  [BACKWARD]: 'backward',
  [FORWARD]: 'forward',
  [FAST_FORWARD]: 'fast-forward',
  [SKIP_FORWARD]: 'skip-forward',
  [PAUSE]: 'pause',
  [STOP]: 'stop',
  [STEP_FORWARD]: 'step-forward',
  [STEP_BACKWARD]: 'step-backward',
}

const DEFAULT_TIPS = {
  [SKIP_BACKWARD]: 'skip to left',
  [FAST_BACKWARD]: 'rewind',
  [BACKWARD]: 'play backward',
  [FORWARD]: 'play forward',
  [FAST_FORWARD]: 'fast-forward',
  [SKIP_FORWARD]: 'skip to right',
  [PAUSE]: 'pause',
  [STOP]: 'stop',
  [STEP_FORWARD]: 'forward 1 frame',
  [STEP_BACKWARD]: 'backward 1 frame',
}

function nextDuration(d, durations) {
  for (let i = 0; i < durations.length; i++) {
    if (isDurationSame(d, durations[i])) {
      return durations[(i + 1) % durations.length]
    }
  }

  return durations[0]
}

const ListingControl = (props) => {

    this.state = {}

  const [interval, setInterval] = useState ();
  const [length, setLength] = useState();
  const [playerState, setPlayerState] = useState();

    if (props.listingManager) {
      setInterval(props.listingManager.interval);
      setLength(props.listingManager.length);
      setPlayerState(props.listingManager.playerState);
      props.listingManager.reListControl(ListingControl)
    }

     
  componentDidUpdate(prevProps) {
    if (prevProps.listingManager !== this.props.listingManager) {
      if (prevProps.listingManager) {
        prevProps.listingManager.deListControl(this)
      }
      if (this.props.listingManager) {
        this.props.listingManager.reListControl(this)
      }
    }
  }

  //componentDidUpdate Replacement Area
   useEffect(() => {
    console.log('component updated!')

  }) 


//componentWillUnmountReplacement
  useEffect(() => {
    return () => {
      props.listingManager.deListControl(ListingControl)
    }
  }, []) // notice the empty array

  const onPlayerCommand = c =>{
    if (this.props.onPlayerCommand) {
      this.props.onPlayerCommand(c)
    }
    if (this.props.listingManager) {
      this.props.listingManager.execute(c)
    }
  }

  const onLengthChange = length => {
    if (this.props.onLengthChange) {
      this.props.onLengthChange(length)
    }
    if (this.props.listingManager) {
      this.props.listingManager.onLengthChange(length)
    }
  }

  const onIntervalChange = interval=> {
    if (this.props.onIntervalChange) {
      this.props.onIntervalChange(interval)
    }
    if (this.props.listingManager) {
      this.props.listingManager.onIntervalChange(interval)
    }
  }

  const getControlsState = () => {
    if (this.props.listingManager) {
      return this.state
    } else {
      return props
    }
  }

  render() {
    let { controls, titles } = this.props
    const { interval, length, playerState } = this.getControlsState()

    controls = {
      SkipBackward,
      FastBackward,
      Backward,
      Forward,
      FastForward,
      SkipForward,
      Pause,
      Stop,
      StepBackward,
      StepForward,
      ...controls,
    }

    titles = {
      ...DEFAULT_TIPS,
      ...titles,
    }

    const stopped = playerState === STOP
    const paused = playerState === PAUSE

    const controlDisabled = {
      [SKIP_BACKWARD]: stopped,
      [FAST_BACKWARD]: stopped,
      [BACKWARD]: false,
      [FORWARD]: false,
      [FAST_FORWARD]: stopped,
      [SKIP_FORWARD]: stopped,
      [PAUSE]: false,
      [STOP]: stopped,
      [STEP_FORWARD]: !paused,
      [STEP_BACKWARD]: !paused,
    }

    const controlProps = { controls, playerState, length, interval }

    const getControl = (c) => {
      const C = controls[NAME_MAPPING[c]]

      const p = {
        className: cssClasses(
          'control',
          CSS_CLASS_MAPPING[c],
          controlDisabled[c] ? 'disabled' : ''
        ),
        onClick: () => this.onPlayerCommand(c),
      }

      if (titles[c]) {
        p.title = titles[c]
      }

      return (
        <div {...p}>
          <C {...controlProps} />
        </div>
      )
    }

    return (
      <div className="timeline-player">
        <div className="controls">
          <div
            className={cssClasses('control', 'interval')}
            onClick={() =>
              this.onIntervalChange(nextDuration(interval, DEFAULT_INTERVALS))
            }
          >
            {moment.duration(interval).as('s')}s
          </div>
          <div className="control sep"> / </div>
          <div
            className={cssClasses(
              'control',
              'length',
              stopped ? '' : 'disabled'
            )}
            onClick={() =>
              stopped &&
              this.onLengthChange(nextDuration(length, DEFAULT_LENGTHS))
            }
          >
            {moment.duration(length).as('s')}s
          </div>
          {getControl(SKIP_BACKWARD)}
          {paused ? getControl(STEP_BACKWARD) : getControl(FAST_BACKWARD)}
          {playerState === BACKWARD
            ? getControl(PAUSE)
            : getControl(BACKWARD)}
          {playerState === FORWARD ? getControl(PAUSE) : getControl(FORWARD)}
          {paused ? getControl(STEP_FORWARD) : getControl(FAST_FORWARD)}
          {getControl(SKIP_FORWARD)}
          {getControl(STOP)}
        </div>
      </div>
    )
  }
}

ListingControl.propTypes = {
  controls: PropTypes.object,
  onPlayerCommand: PropTypes.func,
  playerState: isPlayerState(),
  length: durationLike(),
  onLengthChange: PropTypes.func,
  interval: durationLike(),
  onIntervalChange: PropTypes.func,
  titles: PropTypes.object,
  listingManager: PropTypes.instanceOf(listingManager),
}

ListingControl.defaultProps = {
  playerState: STOP,
  length: 'PT10S',
  interval: 100,
}
export default ListingControl

I'm not sure how this will fit in with an example like

useEffect(() => {
    console.log('component updated!')
  }) 
2

There are 2 answers

5
tomrlh On BEST ANSWER

With useEffect you have a second parameter that is an array with dependencies. Any object that get changed on it, useEffect will listen and re-execute the main callback function. So you can try this:

useEffect(() => {
  // handle changes in prevProps.listingManager here
}, [prevProps.listingManager])

Note: Hooks doesn't work in class components, it are made for functional components

UPDATE:

Considering props.listingManager will be updated and props are read-only, you can workaround it with a local version of props.

const [localListingManager, setLocalListingManager] = useState();

and when props.listingManager get updated, you update the local with the new version:

useEffect(() => {
  setLocalListingManager(props.listingManager)
}, [props.listingManager])

With useEffect dependency list (that is [props.listingManager]) you'll have always the new version of props inside it. What means you don't need to have the prevProps anymore due to localListingManager variable

1
Hossein Mohammadi On

I think migrating to functional components is strange in some situations. I did it before on a huge app. My suggestion is don't try to convert the exact code to hooks, converting the exact code will confuse you. You should understand what you want from the component and rewrite that with hooks, this is easier. Hooks don't behave like a class lifecycle but you could create that behavior with hooks, that is more complicated to say here, I think you should use them as tools, not as a class lifecycle alternative.