jsx-eslint / eslint-plugin-react

React-specific linting rules for ESLint
MIT License
8.99k stars 2.77k forks source link

Rule proposal: warn against using findDOMNode() #678

Closed gaearon closed 8 years ago

gaearon commented 8 years ago

There are almost no situations where you’d want to use findDOMNode() over callback refs. We want to deprecate it eventually (not right now) because it blocks certain improvements in React in the future.

For now, we think establishing a lint rule against it would be a good start. Here’s a few examples of refactoring findDOMNode() to better patterns.

findDOMNode(this)

Before:

class MyComponent extends Component {
  componentDidMount() {
    findDOMNode(this).scrollIntoView();
  }

  render() {
    return <div />
  }
}

After:

class MyComponent extends Component {
  componentDidMount() {
    this.node.scrollIntoView();
  }

  render() {
    return <div ref={node => this.node = node} />
  }
}

findDOMNode(stringDOMRef)

Before:

class MyComponent extends Component {
  componentDidMount() {
    findDOMNode(this.refs.something).scrollIntoView();
  }

  render() {
    return (
      <div>
        <div ref='something' />
      </div>
    )
  }
}

After:

class MyComponent extends Component {
  componentDidMount() {
    this.something.scrollIntoView();
  }

  render() {
    return (
      <div>
        <div ref={node => this.something = node} />
      </div>
    )
  }
}

findDOMNode(childComponentStringRef)

Before:

class Field extends Component {
  render() {
    return <input type='text' />
  }
}

class MyComponent extends Component {
  componentDidMount() {
    findDOMNode(this.refs.myInput).focus();
  }

  render() {
    return (
      <div>
        Hello, <Field ref='myInput' />
      </div>
    )
  }
}

After:

class Field extends Component {
  render() {
    return (
      <input type='text' ref={this.props.inputRef} />
    )
  }
}

class MyComponent extends Component {
  componentDidMount() {
    this.inputNode.focus();
  }

  render() {
    return (
      <div>
        Hello, <Field inputRef={node => this.inputNode = node} />
      </div>
    )
  }
}

Other cases?

There might be situations where it’s hard to get rid of findDOMNode(). This might indicate a problem in the abstraction you chose, but we’d like to hear about them and try to suggest alternative patterns.

gaearon commented 8 years ago

@taion

The case where this comes up most acutely is having a positioner component for e.g. tooltips and overlays.

<Button ref={c => { this.button = c; }} />
<Position target={this.button}>
  <Tooltip>Some tooltip</Tooltip>
</Position>

For to work as a generic component that can e.g. calculate the size of its child, then position it appropriately relative to its target, it needs to be able to get the child's DOM node.

Please correct me if I’m wrong, but in this use case it seems like Position content would not be a part of the downward rendering tree anyway. I’m assuming that <Position> is going to render <Tooltip> to a different tree root (i.e. as a portal).

If my assumption is correct, you render this.props.children into another <div> anyway (like this this._mountNode). So you should have access to this._mountNode.firstChild as well. Sure, it’s not “Reacty”, but that’s the point: you are intentionally breaking enapsulation and accessing the platform-specific tree, so it makes sense that you use platform APIs for this.

Or maybe I misunderstand what <Position> does (which is why I asked for more complete examples).

(Note: it’s an official team’s position, just something I think makes sense. I may be wrong.)

gaearon commented 8 years ago

@jquense

to add to @taions point, so also for focus management, which is required for accessible components, right now react just offers nothing useful for that problem which requires dropping down to DOM nodes, and not wrapping ones in the HOC case. imperative methods sort of work but are often and frustratingly obscured by HOCs, and just not possible for function components.

I’ve used the “ref prop” pattern extensively and found it to work really well for this. I believe I’ve specifically shown this example in the first post but I will repeat it with some comments:

class Field extends Component {
  render() {
    return (
      // Note: ref={this.props.inputRef}. I'm delegating ref to a prop.
      // This will work through many layers of HOCs because they pass props through.
      <input type='text' ref={this.props.inputRef} />
    )
  }
}

class MyComponent extends Component {
  componentDidMount() {
    // It doesn't matter how many levels of HOCs above I am:
    // I will still get this DOM node right here.
    this.inputNode.focus();
  }

  render() {
    return (
      <div>
        {/* Please note: I’m not using ref={}, I’m using my own inputRef={} prop.*/}
        Hello, <Field inputRef={node => this.inputNode = node} />
      </div>
    )
  }
}

And this pattern works just as fine with functional components as it does with classes. If you think it’s problematic can you please point out why?

jquense commented 8 years ago

And this pattern works just as fine with functional components as it does with classes. If you think it’s problematic can you please point out why?

The main problem is that it requires all potential children to expose this sort of API. For an environment that owns all their components fine, that is a pattern that works*. Most of us however are cobbling together applications from lots of smaller libraries, getting every one of them to expose a ref prop is unlikely and fraught.

*More to the pattern itself though, it requires the consumed child to make the choice about whether it will expose it's DOM node. At first glance this sounds like encapsulation but its not; I'd argue its actually leaking implementation details. A child component cannot reasonably make the choice that no consuming parent will ever need to access its rendered node. The Api's that depend on/need findDOMNode are exactly the sort that don't work with this pattern. Positioning for instance is a concern that belongs to the parent not the child. It's unfortunate that the DOM doesn't allow us to express this in terms of the parent always (flexbox does which is one reason its great) but providing the escape hatch, for this DOM limitation, doesn't belong to the child.

What would you expect to get as a reference to a functional component?

The the DOM node of the component, like host components. Sorry for being a bit unclear my problem with the lack of ref-able function components is not that you don't get an instance (agree that makes no sense). The real problem is the same as i'm describing above, its that the component has to make an arbitrary choice about whether it allows an escape hatch to be used, i.e. accessing the underlying host node. Again, this is just not a choice a component can make, it has zero information necessary to make that choice. (I know you can wrap them in a stateful component, but come on, the DX of that is terrible). So we are in positions where we have to not use Function components on the off chance one of them needs to be measured in order to render a tooltip e.g. @taion's above example.

I think both me and @taion are on board that most times you should be using a ref for host node access. What is concerning though is that the escape hatches that make React super flexible and un-frustrating when working with the DOM are slowly being neutered by placing them behind gatekeepers that lack the context necessarily to allow their use.

There seems to be this side effect of api design choices that makes escape hatches harder and harder to use. This does de-incentivize use, but that hurts those of us that need them and makes it harder to provide helpful components and encapsulate the use of those escape hatches away from the consumer. Instead what's happening is we need to now involve our users more and more in the ugliness of FFI use by requiring they expose props for refs, or not use Function components. This seems counter intuitive, I'd be nicer to see more work going into solving the reasons why ppl need these escape hatches before actually or effectively removing them.

Sorry if my tone is harsh, It stems from a these changes that make our lives harder as maintainers. I realize also that folks may not have the same issues as us at React-Bootstrap, but most UI libs are not actively dealing with interop with non-react stuff. I feel like our use case is important, and also a bit more indicative of what folks working on brownfield applications face, especially those who don't post on the eslint-react-plugin repo :P

jquense commented 8 years ago

also a quick side point, CSS flexbox is extremely intolerant to extra wrapping nodes. On one hand this is one reason why fragments would be awesome, but that would be negated by HoC's needing to wrap in extra divs :P

lencioni commented 8 years ago

@gaearon yes! I actually came to the same realization late last night. Thanks for being my rubber duck.

I did make one modification in case the component renders null:

const elem = container.firstChild instanceof Element
  ? container.firstChild
  : container;
gaearon commented 8 years ago

Positioning for instance is a concern that belongs to the parent not the child.

In case of positioning, don’t you already pull the child out into a new tree where you have access to the DOM? See https://github.com/yannickcr/eslint-plugin-react/issues/678#issuecomment-237840983.

A child component cannot reasonably make the choice that no consuming parent will ever need to access its rendered node.

I’m not sure I’m following without a specific example. (I answered to all specific examples above so far, I think.)

You mentioned the focus use case, but isn’t it reasonable for third party React input components to always provide imperative focus() method? I don’t really see why this would be unexpected for those component libraries. Again, a specific example we could look at would be helpful here.

jquense commented 8 years ago

@gaearon not all of positioning involves subtrees. @taion's example is a good one still: the Button can have a ref, but you need to use findDOMNode to access the position info in order to display the overlay.

<Button ref={c => { this.button = findDOMNode(c); }} />
<Position target={this.button}>
  <Tooltip>Some tooltip</Tooltip>
</Position>

Or:

class ButtonWithTooltip extends React.Component {
  componentDidMount() {
     renderToolTipInSubtree({ 
        target: ()=> findDOMNode(this)
     })
  },
  render() {
    // cannot wrap in a div, as it will not longer work in `ButtonToolbar` or ModalFooter
    return <Button {...this.props}/>
  }
}

The above is a good example of who should own the use of the escape hatch. Button should not have to think about whether someone might want to display the tooltip. I don't think it makes sense for the button author to ask themselves: "May this need to be measured? Maybe! so no Function component and provide a ref prop"

The other use case is needing to measure, oneself.

class ElementQuery extends React.Component {
  componentDidUpdate() {
    let node = findDOMNode(this);
    let visible = node.offsetWidth > this.props.width;

    if (visible !== this.state.visible) 
      this.setState({ visible })
  }
  render() { 
    if (this.state.visible)
      return React.Children.only(this.props.children)
    return null;
  }
}

You mentioned the focus use case, but isn’t it reasonable for third party React input components to always provide imperative focus() method?

I agree though, again, the use HoC's for everything and Function components makes imperative methods mostly not an option anymore. Being able to drill down to the component node to focus it is often the only viable option, and well behaved input components should handle a native focus() correctly.

The above are simplistic examples of course that could have alternatives, I am providing them in the context of earlier conversation about how wrapping nodes aren't always feasible, and buttonRef props being leaky and semantically wrong.

taion commented 8 years ago

In the position example, a lot of cases can just be handled with position: absolute. No need for a subtree there.

gaearon commented 8 years ago

Button should not have to think about whether someone might want to display the tooltip. I don't think it makes sense for the button author to ask themselves: "May this need to be measured? Maybe! so no Function component and provide a ref prop"

I’m not quite convinced components that require this don’t already know this.

Do I understand correctly that the only reason why you have to use Button’s DOM node (and not a wrapping node) is because it’s a part of a CSS framework like Bootstrap which forces direct parent-child relationships and doesn’t allow extra DOM nodes?

It seems that Button then knows it’s in an intimate relationship with another component (such as ButtonGroup), and that it actually knows that it should expose its node to work around such issues.

If it was a truly independent component, it would work with a wrapping <div> just fine, and thus wouldn’t need to expose anything. But since it’s not, it looks like the Button has all the necessary knowledge to opt into sharing its node.

I can ask the same question about flex children: don’t they already know they are flex children and rely on having a specific parent, and thus need to expose their nodes just in case something else wants to access them?

The ergonomics of exposing the node is a different question (it could be made as simple as forwarding a ref without need for custom props). Ref forwarding would similarly solve the HOC problem in the same way.

But I wonder what are the cases where you can’t wrap a component in a <div> and that component isn’t aware of this.

taion commented 8 years ago

We're trying to write primitives that can be used in multiple contexts. React-Bootstrap is one of those contexts, but it's not the only one.

Every external CSS library that needs to interop with React for things like popovers should not be required to bundle its own specific overlay wrapper.

gaearon commented 8 years ago

I’m not quite sure I understand. I don’t suggest changes to CSS libraries.

I’m only saying that React components that wrap those CSS libraries generally seem to know when they’re relying on something like "component A must be direct child of component B".

And so in this case, with proposal like https://github.com/facebook/react/issues/4213, they would need to add something like

getPublicInstance() {
  return this.node;
}

for their refs to get resolved to their node.

Would this not work for you?

It is not clear to me what you mean in the last comment so perhaps code again would help.

jquense commented 8 years ago

I’m not quite convinced components that require this don’t already know this.

I don't think every component should have to think about where someone might want to display a tooltip over it, which is what I mean here.

Do I understand correctly that the only reason why you have to use Button’s DOM node (and not a wrapping node) is because it’s a part of a CSS framework like Bootstrap which forces direct parent-child relationships and doesn’t allow extra DOM nodes?

Its not just bootstrap, somethings just can't be reasonably styled to handle either <button> or <div><button/></div>. In the case of a button group if you need to do something

.btn-group > *:not(:first-child, :last-child) {
  border-radius: 0;
}

// and so on...to get buttons that are neatly styled next to each other

You could right that as .btn-group .btn {} but that makes nesting stuff hard and just is flat out impossible with css-modules see for more context there.

It's easy to say "just write your styles in a way that is agnostic to DOM structure" but that often just not possible.

gaearon commented 8 years ago

Its not just bootstrap, somethings just can't be reasonably styled to handle either <button> or <div><button/></div>

My point was that presumably components using such classNames know they’re one of “those things”, and thus may opt into exposing their nodes like I described here: https://github.com/yannickcr/eslint-plugin-react/issues/678#issuecomment-237894868.

Is there some reason why a component might not be aware that it has to be used in a specific parent DOM structure?

jquense commented 8 years ago

Is there some reason why a component might not be aware that it has to be used in a specific parent DOM structure?

sure, most library components. We provide <Button>, its not possible for us to know that that user is going to write a <ButtonTabList> that uses Button. Or in the case where you are using Button from library A and Tooltip from library B. on a philosophical note.

On a more philosophical point it seems like an odd to me that HoC's should add DOM nodes, when the pattern is often used as a way to compose in behavior, not structure.

To be clear, we don't need findDOMNode specifically, we need a way to get at a component's rendered output without that component needing to explicitly opt in to that, or make it super simple to forward to the right node in the child, in the way that style and className work. The problem is that unless that pattern is explicitly defined by React the contract will differ from library to library making the idea of Overlay, or Position primitive that can be used in a wide range of contexts impossible. e.g. RB will have elementRef, while MUI has nodeRef, etc etc. tho I still consider that solution suboptimal to the current findDOMNode api.

taion commented 8 years ago

The other issue with the "ref prop" approach is that it requires you to have control of everything that you render, if you need to avoid extra DOM nodes.

For example, if <MyComponent> renders a <div>, then it could use a ref prop. However, if <MyComponent> renders something like <MyCustomCard>, then <MyCustomCard> needs to know about that same ref prop and attach that to its DOM node. With additional composition, this adds a lot more clutter.

More problematically, if the top-level view component exists in a separate library, then this becomes close to intractable.

taion commented 8 years ago

FWIW, I don't think we're far apart on this.

It's fine for it to be an opt-in for a component to expose a way to get its own DOM node, but this needs to be an ecosystem-wide convention.

Something like a pass-through nodeRef is unnecessarily clunky, though. Really it sounds like what you're describing is something like an opt-in version of the old getDOMNode.

gaearon commented 8 years ago

sure, most library components. We provide

Do I understand correctly that relying on Button’s className and writing stylesheets using it directly in expressions like .stuff > .button in other components is considered acceptable usage?

jquense commented 8 years ago

Do I understand correctly that relying on Button’s className and writing stylesheets using it directly in expressions like .stuff > .button in other components is considered acceptable usage?

when building on top of a UI framework like bootstrap or similar? sure, the class names are generally considered the api to the css framework. Admittedly React components allow an ergonomic way to avoid that, however, even limiting that wouldn't solve the problem:

.my-button-group {
   display: flex;
   align-items: space-between;
}

.my-button-group > * {
  flex: 1;
}

Uses no reliance on library classNames (and is the recommended approach for css-modules), but would break hard if the button child was wrapped in a div. It is also not easily written to handle an either a button or a button wrapped in a div.

Note that these examples assume one HOC, but having more isn't unreasonable either when you are trying to build lower level composable utilities, think: withTooltip(measurable(Component)) is each supposed to add another div?

gaearon commented 8 years ago

Yeah, I see what you’re saying. Thanks for explaining these use cases, we’ll keep them in mind for the future.

As I said before findDOMNode() is not deprecated, it’s just discouraged. I still think it’s reasonable to have this as a lint rule, as the use case is relatively rare and can easily be confined to a very specific sort of component (like Measurer etc).

jquense commented 8 years ago

I still think it’s reasonable to have this as a lint rule, as the use case is relatively rare and can easily be confined to a very specific sort of component (like Measurer etc).

definately, on the same page there :)

danez commented 8 years ago

I have the following usecase and replacing findDOMNode with the ref returned in the callback makes the code throw when I try to call methods of the DOM node?

import React, { Component } from 'react';

class Align extends Component {
  constructor(props) {
    super(props);
    this.alignRef = null;
  }

  componentDidMount() {
    this.alignRef.getBoundingClientRect();
  }

  render() {
    const childPropsNew = {
      ref: node => { this.alignRef = node; },
    };

    return React.cloneElement(this.children, childPropsNew);
  }
}
Uncaught TypeError: this.alignRef.getBoundingClientRect is not a function

Is it because of the clone or am I doing something wrong?

ljharb commented 8 years ago

With a ref callback that's an arrow function, you will sometimes get null in the ref argument. I'm not sure why this is, though.

danez commented 8 years ago

It is not about the ref being null, the ref is set but it is a ReactElement and not a DOM node. So it works fine when using findDOMNode in the componentDidMount. So I don't understand how it should work without? Is the element instance supposed to proxy DOM methods/properties of the DOM node? Ok I think I got it, I can replace findDOMNode when having refs on browser components( <div /> etc), but if i do not know if my child is ReactComponent/DOMNode I can't get around doing findDOMNode unless I would introduce extra markup around my child with a ref.

ghost commented 8 years ago

i have
<div ref={node => this.domNode = node} style={this.getStyle()}>{ this.props.children }</div> when i do this this.domNode.addEventListener('mousedown', this.onDrag); there is an error this.domNode.addEventListener is not a function

gaearon commented 8 years ago

@ljharb It is expected, you get null for clean up. Every time you pass an arrow, it changes, so we call the old arrow with null, and the new one with the node right away. This is not a perf problem.

@danez @radwa0 If you believe this is a bug in React, please file a bug in React repo with a reproducing case. This is discussion about a lint rule, so it is not a right place to file bugs.

ljharb commented 8 years ago

@gaeron sure, makes sense that it's not a perf problem - but if I haven't unmounted, I wouldn't expect to get anything but a node. Essentially, that's a gotcha to code around in arrow ref callbacks - I'll have to do node => { if (node) { this.foo = node; } }

gaearon commented 8 years ago

On the opposite, it is deliberate that we pass null on unmounting so that your field gets nulled too. This is to prevent potential memory leaks.

gaearon commented 8 years ago

Even before unmounting, you are passing a new function. So it gives null to the old function and node to the new function. There is normal (even if a bit non-obvious) and there is no reason to guard against this. Your code should already handle null because it has to handle unmounting. Please trust React to call it with the node after calling it with null during the update. React has tests for this. :wink:

What am I missing?

ljharb commented 8 years ago

Are there any lifecycle methods or event handlers that could get called between the old callback and the new one? We've seen bugs caused by other code expecting a node, but receiving null.

gaearon commented 8 years ago

I don’t know the ref code that well but if you had issues with them, please try to isolate them and file bugs against React.

As far as I can see from the source, the refs get detached (called with null) before the internal state related to <div> (“the host instance”) is updated. Later, all refs get attached (called with DOM nodes) during the flush, which is currently synchronous in React unless you use unsupported extensions (like rAF rendering). There is indeed a period where you might not have refs, but as far as I see we don’t call lifecycle methods on the component itself during this period.

I can imagine that if <A> contains <B>, and <B> calls a callback passed down from <A> in its componentWillUpdate, and that callback accesses a ref to <B> on <A>, it might be null at this point. I’m not sure why we don’t just keep the old ref attached in this case but this is really a discussion we should have in a React issue.

webdif commented 8 years ago

All the examples above talk about callback ref on a <div>.

But what about callback ref on Component? It will give us the Component object for class, or null for pure functions. To get the DOM node, we have to transform the stateless function into a class, and use findDOMNode. How can you get the dom node without it?

fhelwanger commented 8 years ago

@webdif Take a look at findDOMNode(childComponentStringRef) here https://github.com/yannickcr/eslint-plugin-react/issues/678#issue-165177220.

I think it's what you're looking for.

webdif commented 8 years ago

@fhelwanger Thanks a lot 🎉 I did not update the child component, that's why it did not work. Make a lot of sense, even if it is not very convenient to do so!

petersendidit commented 8 years ago

Should we enhance the doc's for this rule to show a few more of these examples? Seems like this is going to be a common issue while the community learns how to use refs without findDOMNode.

webdif commented 8 years ago

There is already a link to this issue in the no-find-dom-node.md, but a more detailed doc could be useful, to avoid other developers to make the mistake I made between findDOMNode(this) and findDOMNode(childComponentStringRef).

rhys-vdw commented 8 years ago

Just updated to latest and started getting this error. Turns out I have a use case for findDOMNode that doesn't fall super neatly into the above categories. Actually I worked out a solution while I was writing this comment, but I'll post it anyway in case it's useful to anyone (or someone knows a cleaner solution).

I have a decorator called Dimensions that will set probs width and height on the decorated component.

(not the complete code):

export default function DimensionsDecorator(DecoratedComponent) {

  class DimensionsContainer extends Component {
    constructor(props) {
      super(props)
      this.state = { width: 0, height: 0 }
      this.handleWindowResize = debounce(
        this.handleWindowResize.bind(this),
        RESIZE_DEBOUNCE_DELAY_MS
      )
    }

    componentDidMount() {
      window.addEventListener('resize', this.handleWindowResize)
      this.updateDimensions()
    }

    componentWillUnmount() {
      window.removeEventListener('resize', this.handleWindowResize)
    }

    handleWindowResize(event) {
      this.updateDimensions()
    }

    updateDimensions() {
      // eslint-disable-next-line react/no-find-dom-node
      const element = findDOMNode(this.innerComponent)
      if (!isHidden(element)) {
        const width = element.offsetWidth
        const height = element.offsetHeight
        this.setState({ width, height })
      }
    }

    render() {
      const { width, height } = this.state
      return (
        <DecoratedComponent
          ref={innerComponent => this.innerComponent = innerComponent}
          width={width}
          height={height}
          {...this.props}
        />
      )
    }
  }

  return DimensionsContainer
}

So, looking at the suggestions above, the closest fit I can think of is requiring components to also implement a containerRef prop perhaps? Something like this?

    componentDidMount() {
      if (this.containerElement == null) {
        if (process.env.NODE_ENV === 'development') throw new Error(
          `DimensionsDecorator expected component ${DecoratedComponent.name} ` +
          `to handle \`containerRef\` prop`
        )
      }
      window.addEventListener('resize', this.handleWindowResize)
      this.updateDimensions()
    }

    render() {
      const { width, height } = this.state
      return (
        <DecoratedComponent
          containerRef={element => this.containerElement = element}
          width={width}
          height={height}
          {...this.props}
        />
      )
    }
5angel commented 8 years ago

Consider the following:

  1. A have a component from a third-party library (say, it's "material ui")
  2. I want to get a dom node from it
  3. It doesn't have a ref, though

What do?

cAstraea commented 7 years ago

How would you rewrite this ? Not sure I understand how the new ref method works :( describe('Render', () => { it('should render clock', () => { const clock = TestUtils.renderIntoDocument(); const $el = $(ReactDOM.findDOMNode(clock)); const actualText = $el.find('.clock-text').text();

expect(actualText).toBe('01:02');

}); });

Should I render into document while passing the ref ?

okonet commented 7 years ago

@cAstraea you can use https://github.com/airbnb/enzyme

taion commented 7 years ago

Again, to be clear, we don't have a problem per se with encouraging refs – but we really need the React team to promulgate some sort of standard for "this is the name of the prop to use for the ref to get to the DOM node". Otherwise interop across component libraries is a hopeless cause.

Just settle on nodeRef or elementRef or whatever. But pick something and give it the official imprimatur. I'm not interested in inventing something myself only to find that other libraries use different conventions, and we're stick with no ability to interoperate for months without extra shims. That's no good.

taion commented 7 years ago

(and ideally also make it work appropriately with DOM elements)

kopax commented 7 years ago

I was using

  handleClick = () => {
    const collapseEl = ReactDOM.findDOMNode(this).querySelector('.collapse');
    collapseEl.classList.toggle('active');
  }

I don't see how this notation can help

  handleClick = () => {
    const collapseEl = this.node.querySelector('.collapse');
    collapseEl.classList.toggle('active');
  }

querySelector doesn't exist. Is there a way to target an element to add a css3 class, I need to trigger a css3 effect and state wasn't very helpful for that job !

webdif commented 7 years ago

@kopax If you're doing it right, querySelector should exists, as the ref callback is returning a HTMLElement object. You must have a render method like this:

render() {
  return (
    <div ref={(node) => { this.node = node; }}>
      <div className="collapse" />
      <button onClick={this.handleClick} />
    </div>
  );
}

But, maybe you don't need ref alltogether. The best way to enjoy React is to embrace the declarative way, and just change the state when something happen. For example :

state = {
  active: false,
}

handleClick = () => {
  this.setState({ active: true });
}

render() {
  return (
    <div>
      <div className={`collapse ${this.state.active ? 'active' : ''}`} />
      <button onClick={this.handleClick} />
    </div>
  );
}

With or without state, it just adds a class, thus it triggers the css effect.

ljharb commented 7 years ago

(I edited the above comment to remove the ref callback entirely in the latter example)

alextkachuk commented 7 years ago

@gaearon There is one more case that a component needs to know about click outside.

  componentWillMount() {
    document.addEventListener('click', this.onOutsideClick, false);
  }

  componentWillUnmount() {
    document.removeEventListener('click', this.onOutsideClick, false);
  }
onOutsideClick(event) {
    const domNode = ReactDOM.findDOMNode(this);

    if (!domNode.contains(event.target) && this.state.isOpen) {
      this.setState({ isOpen: false });
    }
  }

Using ref:

<div ref={(node) => { this.mainNode = node; }}>
  ----
</div>
onOutsideClick(event) {
    if (!this.mainNode.contains(event.target) && this.state.isOpen) {
      this.setState({ isOpen: false });
    }
  }
andrevenancio commented 7 years ago

I might be missing something, but trying to get the element dom height or any other property coming from (let's say) getBoundingClientRect() simply calling this.node.getBoundingClientRect() will return undefined. this on the latest version of react.

import React from 'react';
class Bla extends React.Component {
    componentDidMount() {
        console.log(this.node.getBoundingClientRect());
    }

    render() {
        return (
            <div ref={(node) => this.node = node}>blaaaaa</div>
        );
    }
}
export default Bla;
webdif commented 7 years ago

@andrevenancio If you store your DOM element in this.node, why do you use this.foo? Maybe, it's a typo?

import React from 'react';
class Bla extends React.Component {
    componentDidMount() {
        console.log(this.node.getBoundingClientRect());
    }

    render() {
        return (
            <div ref={(node) => this.node = node}>blaaaaa</div>
        );
    }
}
export default Bla;
andrevenancio commented 7 years ago

yeah that was a typo. corrected my comment.

fhelwanger commented 7 years ago

Just copied and pasted your code.... it works. http://codepen.io/anon/pen/peRPrz?editors=0010

It must be something else in your code. Try to create a minimal example that reproduces your problem.

gavinwahl commented 7 years ago

The one case where I was not able to use a callback ref instead of findDOMNode is with ReactCSSTransitionGroup. There's no other way to get the DOM node used as the wrapper element, because a ref on the ReactCSSTransitionGroup just gives you a ref to that component, not the DOM node.