ReactTraining / react-media

CSS media queries for React

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Use React 16.3 lifecycle methods

thebuilder opened this issue · comments

Hey!

Since componentWillMount is considered unsafe in the latest version of React, i looked into replacing it with componentDidMount.

I've integrated the component into my project (rewritten it to use Flow types, and removed json2mq since i didn't need it).
If you'd like, i can update the current module to follow this structure.

The biggest change is that i've added a ssr prop. If this is false (default), the client will add the matcher in the constructor method. Otherwise, the match will not happen before componentDidMount. This is to ensure the DOM doesn't change before the first render.
This change will eliminate the DOM doesn't match when rendering serverside.
facebook/react#10591

I don't think this will break existing solutions, since the ssr=false behavior matches the current implementation.

I've also added support for componentDidUpdate, so the query can be changed at runtime (hot reload).

// @flow
import * as React from 'react'

type MediaQueryList = {
  matches: boolean,
  addListener: (handler: Function) => void,
  removeListener: (handler: Function) => void,
}

type Props = {
  defaultMatches: boolean,
  query: string,
  render?: Function,
  children?: React.Node | Function,
  ssr?: boolean,
}

type State = {
  matches: boolean,
}

/**
 * Conditionally renders based on whether or not a media query matches.
 * Adapted from: https://github.com/ReactTraining/react-media
 */
class MediaQuery extends React.Component<Props, State> {
  static defaultProps = {
    defaultMatches: true,
    ssr: false,
  }

  constructor(props: Props) {
    super(props)
    this.state = {
      matches: this.props.defaultMatches,
    }

    if (!props.ssr) {
      // If not rendering serverside, add the matcher now so state reflects the correct query
      this.addMatcher()
    }
  }

  componentDidMount() {
    if (!this.mediaQueryList) {
      this.addMatcher()
    }
  }

  componentDidUpdate(lastProps: Props) {
    if (this.props.query !== lastProps.query) {
      this.addMatcher()
    }
  }

  componentWillUnmount() {
    if (this.mediaQueryList) {
      this.mediaQueryList.removeListener(this.updateMatches)
    }
  }

  mediaQueryList: MediaQueryList

  addMatcher() {
    if (this.mediaQueryList) {
      this.mediaQueryList.removeListener(this.updateMatches)
    }
    this.mediaQueryList = window.matchMedia(this.props.query)
    this.mediaQueryList.addListener(this.updateMatches)
    this.updateMatches()
  }

  updateMatches = () => this.setState({ matches: this.mediaQueryList.matches })

  render() {
    const { children, render } = this.props
    const { matches } = this.state

    return render
      ? matches
        ? render()
        : null
      : children
        ? typeof children === 'function'
          ? children(matches)
          : !Array.isArray(children) || children.length // Preact defaults to empty children array
            ? matches
              ? React.Children.only(children)
              : null
            : null
        : null
  }
}

export default MediaQuery

It's not a good idea to add matcher in the constructor, as long componentWillMount was removed as long Will does not mean Did. So the same rules should be applied to a constructor.

Take a look on react-media-matcher, which handles this moment more correctly.

class Media extends React.Component{
  constructor(props: MediaProps) {
    super(props);

    if (typeof window !== "object" || !window.matchMedia) return;

    this.mediaQueryList = window.matchMedia(this.props.query);
    this.state.matches = this.mediaQueryList.matches; <-- just SET IT
  }

  updateMatches = () => this.setState({matches: this.mediaQueryList!.matches});

  componentDidMount() {
    if (this.mediaQueryList) {
      this.mediaQueryList.addListener(this.updateMatches); <-- subscribe!
    }
  }

  componentWillUnmount() {
    if (this.mediaQueryList) {
      this.mediaQueryList.removeListener(this.updateMatches);
    }
  }

  render() {
    const {children} = this.props;
    const {matches} = this.state;

    return children(matches)
  }
}

The thing is, you'll get an output mismatch if rendering serverside, when the query doesn't match the default match.

That's the reason for the ssr prop. It adds the matcher if only rendering on the client.

In my case I do not use Media component to render something, only to calculate match. Next results for all Medias for all my breakpoints got stored in context, and every renderer read value not from Media, but from context.
For SSR you may put any custom value into that context, and thus put application render into the desired state.

Don't use Media directly - that's the only way to make your application a bit more controllable and predictable.

@thebuilder Thanks for the input! I'm closing this in favor of #81. If you feel like it, we'd appreciate a PR with the solution outlined in that issue.

P.S. I'm not sure how @mjackson feels about rewriting the library with static types 🙈