javascriptreactjsmediaelement.js

React Child component will mount then unmount after calling parent function to set parent state


I have two components, parent and child. The parent keeps track of the audio player component (the child is the player) and what segment the player is playing, e.g. segment 1 might be the first 34 seconds followed by the second segment until 215 seconds, etc.

My parent component renders the Player component and passes a bound function to the Player so the Player can update the parent with the current time of the player so the parent can figure out which segment should be highlighted.

The problems are that (1) (major problem) once play button is clicked and it plays, or the user skips, beyond the first segment break then the state of the parent updates but the Player is unmounted, causing the MediaElement to be removed; (2) (minor problem) when initially loading the page, the Player unmounts, followed by the parent mounting, followed by the Player unmounting and mounting again. I believe they're related.

parent:

import React from 'react'
import shortid from 'shortid'

import Frame from '../../layout/Frame'
import Box from '../../layout/Box'
import Flex from '../../layout/Flex'
import G1 from '../../layout/G1'

import Player from '../../parts/Player'

import BriefingTitle from './BriefingTitle'

import {assoc, lensPath, set, view} from 'ramda'
import {createMarkup} from '../../../lib/tools'

class Briefing extends React.Component {
  constructor({briefing}) {
    super()

    const segments = briefing.segments.map(assoc('playing', false))
    console.log('segments:', segments)
    this.state = {
      briefing,
      segments
    }
    this.parentMonitor = this.updateSegments.bind(this)
  }

  updateSegments(time) {
    console.log('time:', time)
    const firstPlayingLens = lensPath([0, 'playing'])
    if (time > 36 && !view(firstPlayingLens, this.state.segments)) {
      this.setState(set(firstPlayingLens, true, this.state.segments))
    }
  }

  componentDidMount() {
    console.log('Briefing mounted')
  }

  componentWillUnmount() {
    console.log('Briefing will unmount')
  }

  render() {
    const {briefing, segments} = this.state
    return (
      <Frame pb={['0px', 3]}>
        <G1>
          <Flex pt={[2, 3]} direction={['column', 'row']}>
            <Box mt={[2, 'm']} mr={2} shrink={0} grow={2} order={[2, 1]}>
              <BriefingTitle><span dangerouslySetInnerHTML={createMarkup(briefing.title)} /></BriefingTitle>

              <Box mt={0} pt={0} bt>
                <Player key={'briefing_'+briefing.id} url={briefing.audioFile} type="audio/mp3" duration={briefing.duration} parentMonitor={this.parentMonitor}>Play Full Episode</Player>
              </Box>
              <Box mt={0} pt={0} bt>
                {briefing.segments.map(s => s.playing ? <p><strong>{s.title}</strong></p> : <p>{s.title}</p>)}
              </Box>
            </Box>
          </Flex>
        </G1>
      </Frame>
    )
  }
}

export default Briefing

Player:

import React from 'react'
import styled from 'styled-components'

import Flex from '../../layout/Flex'
import Box from '../../layout/Box'

import 'mediaelement'
import 'mediaelement/build/mediaelementplayer.min.css'
import 'mediaelement/build/mediaelement-flash-video.swf'
import 'mediaelement-plugins/dist/skip-back/skip-back.min.js'
import 'mediaelement-plugins/dist/skip-back/skip-back.css'

import {rem} from '../../../lib/tools'
import {type} from '../../../designSystem'

const StyledSpan = styled.span`
  font-family: ${type.family.default};
  font-size: ${rem(type.size.s0)};
  font-weight: ${type.weight.bold};
  line-height: ${type.lineHeight.meta};
`

class Player extends React.Component {
  constructor(props, {
    inverse = props.inverse ? true : false
  }) {
    super()
    this.state = {
      inverse,
      children: props.children,
      player: null
    }
  }

  monitor(media) {
    this.props.parentMonitor(media.getCurrentTime())
    setTimeout(this.playing.bind(this), 200)
  }

  playing() {
    this.monitor(this.state.player)
  }

  success(media, node, instance) {
    // successfully loaded!
    const playEvent = e => this.playing()
    media.addEventListener('playing', playEvent)
    media.removeEventListener('pause', playEvent)
    media.removeEventListener('ended', playEvent)
  }

  error(media) {
    // failed to load
  }

  componentDidMount() {
    console.log('Player mounted')
    const {MediaElementPlayer} = global
    if (MediaElementPlayer) {
      const options = {
        features: ['skipback'],
        useDefaultControls: true,
        pluginPath: './build/static/media/',
        skipBackInterval: 31,
        skipBackText: 'Rewind 30 seconds',
        success: (media, node, instance) => this.success(media, node, instance),
        error: (media, node) => this.error(media, node)
      }
      this.setState({player: new MediaElementPlayer('player_'+this.props.key, options)})
    }
  }

  componentWillUnmount() {
    console.log('Player will unmount')
    if (this.state.player) {
      this.state.player.remove()
      this.setState({player: null})
    }
  }

  shouldComponentUpdate() {
    return false
  }

  render() {
    return (
      <Flex justify={this.state.children ? 'space-between' : ''} align="center">
        <Flex align="center">
          <audio id={'player_'+this.props.key} width={this.props.width || 400}>
            <source src={this.props.url} type={this.props.type} />
          </audio>
        </Flex>
      </Flex>
    )
  }
}

export default Player

I'm using MediaElement and React 15.5.4.


Solution

  • With @Hoyen's help to figure out that re-rendering was caused by the parent's state change, I figured out I needed to separate the state of the parent from the state of the segments. I put the segments in their own child class and called them from the parent when the player's time updated.

    Note (in the parent) the call to the segment child this.refs.segments.updateSegments and the ref attribute in the parent render of the Segments, <Segments ref="segments" key={"bSegments"+this.state.briefing.id} segments={segments}></Segments> that makes it possible to call the child component.

    Parent:

    import React from 'react'
    import shortid from 'shortid'
    
    import Frame from '../../layout/Frame'
    import Box from '../../layout/Box'
    import Flex from '../../layout/Flex'
    import G1 from '../../layout/G1'
    
    import Player from '../../parts/Player'
    import Segments from '../../parts/Player/Segments'
    
    import BriefingTitle from './BriefingTitle'
    
    import {assoc, lensPath, set, view} from 'ramda'
    import {createMarkup} from '../../../lib/tools'
    
    class Briefing extends React.Component {
      constructor({briefing}) {
        super()
    
        const segments = briefing.segments.map(assoc('playing', false))
        console.log('segments:', segments)
        this.state = {
          briefing,
          segments
        }
        this.parentMonitor = this.updateSegments.bind(this)
      }
    
      updateSegments(time) {
        this.refs.segments.updateSegments(time)
      }
    
      componentDidMount() {
        console.log('Briefing mounted')
      }
    
      componentWillUnmount() {
        console.log('Briefing will unmount')
      }
    
      render() {
        const {briefing, segments} = this.state
        console.log('render Briefing')
        return (
          <Frame pb={['0px', 3]}>
            <G1>
              <Flex pt={[2, 3]} direction={['column', 'row']}>
                <Box mt={[2, 'm']} mr={2} shrink={0} grow={2} order={[2, 1]}>
                  <BriefingTitle><span dangerouslySetInnerHTML={createMarkup(briefing.title)} /></BriefingTitle>
    
                  <Box mt={0} pt={0} bt>
                    <Player key={'briefing_'+briefing.id} url={briefing.audioFile} type="audio/mp3" duration={briefing.duration} parentMonitor={this.parentMonitor}>Play Full Episode</Player>
                  </Box>
                  <Segments ref="segments" key={"bSegments"+this.state.briefing.id} segments={segments}></Segments>
                </Box>
              </Flex>
            </G1>
          </Frame>
        )
      }
    }
    
    export default Briefing
    

    Player:

    import React from 'react'
    import styled from 'styled-components'
    
    import Flex from '../../layout/Flex'
    import Box from '../../layout/Box'
    
    import 'mediaelement'
    import 'mediaelement/build/mediaelementplayer.min.css'
    import 'mediaelement/build/mediaelement-flash-video.swf'
    import 'mediaelement-plugins/dist/skip-back/skip-back.min.js'
    import 'mediaelement-plugins/dist/skip-back/skip-back.css'
    
    import {rem} from '../../../lib/tools'
    import {type} from '../../../designSystem'
    
    const StyledSpan = styled.span`
      font-family: ${type.family.default};
      font-size: ${rem(type.size.s0)};
      font-weight: ${type.weight.bold};
      line-height: ${type.lineHeight.meta};
    `
    
    class Player extends React.Component {
      constructor(props, {
        inverse = props.inverse ? true : false
      }) {
        super()
        this.state = {
          inverse,
          children: props.children,
          player: null
        }
      }
    
      monitor(media) {
        this.props.parentMonitor(media.getCurrentTime())
        setTimeout(this.playing.bind(this), 200)
      }
    
      playing() {
        this.monitor(this.state.player)
      }
    
      success(media, node, instance) {
        // successfully loaded!
        const playEvent = e => this.playing()
        media.addEventListener('playing', playEvent)
        media.removeEventListener('pause', playEvent)
        media.removeEventListener('ended', playEvent)
      }
    
      error(media) {
        // failed to load
      }
    
      componentDidMount() {
        console.log('Player mounted')
        const {MediaElementPlayer} = global
        if (MediaElementPlayer) {
          const options = {
            features: ['skipback'],
            useDefaultControls: true,
            pluginPath: './build/static/media/',
            skipBackInterval: 31,
            skipBackText: 'Rewind 30 seconds',
            success: (media, node, instance) => this.success(media, node, instance),
            error: (media, node) => this.error(media, node)
          }
          this.setState({player: new MediaElementPlayer('player_'+this.props.key, options)})
        }
      }
    
      componentWillUnmount() {
        console.log('Player will unmount')
        if (this.state.player) {
          this.state.player.remove()
          this.setState({player: null})
        }
      }
    
      shouldComponentUpdate() {
        return false
      }
    
      render() {
        console.log('render player')
        return (
          <Flex justify={this.state.children ? 'space-between' : ''} align="center">
            <Flex align="center">
              <audio id={'player_'+this.props.key} width={this.props.width || 400}>
                <source src={this.props.url} type={this.props.type} />
              </audio>
            </Flex>
          </Flex>
        )
      }
    }
    
    export default Player
    

    Segments:

    import React from 'react'
    
    import Box from '../../layout/Box'
    
    import {lensPath, set, view} from 'ramda'
    
    class Segments extends React.Component {
      constructor(props) {
        super()
    
        this.state = {
          segments: props.segments
        }
      }
    
      updateSegments(time) {
        console.log('time:', time)
        const firstPlayingLens = lensPath([0, 'playing'])
        if (time > 36 && !view(firstPlayingLens, this.state.segments)) {
          const modifiedSegments = set(firstPlayingLens, true, this.state.segments)
          console.log('modifiedSegments:', modifiedSegments)
          this.setState({segments: modifiedSegments})
        }
      }
    
      render() {
        console.log('render Segments')
        return (
          <Box mt={0} pt={0} bt>
            {this.state.segments.map(s => s.playing ? <p><strong>{s.title}</strong></p> : <p>{s.title}</p>)}
          </Box>
        )
      }
    }
    
    export default Segments