88

I am looking to create a stateless component who's input element can be validated by the parent component.

In my example below, I am running into a problem where the input ref is never being assigned to the parent's private _emailAddress property.

When handleSubmit is called, this._emailAddress is undefined. Is there something I'm missing, or is there a better way to do this?

interface FormTestState {
    errors: string;
}

class FormTest extends React.Component<void, FormTestState> {
    componentWillMount() {
        this.setState({ errors: '' });
    }

    render(): JSX.Element {
        return (
            <main role='main' className='about_us'>             
                <form onSubmit={this._handleSubmit.bind(this)}>
                    <TextInput 
                        label='email'
                        inputName='txtInput'
                        ariaLabel='email'
                        validation={this.state.errors}
                        ref={r => this._emailAddress = r}
                    />

                    <button type='submit'>submit</button>
                </form>
            </main>
        );
    }

    private _emailAddress: HTMLInputElement;

    private _handleSubmit(event: Event): void {
        event.preventDefault();
        // this._emailAddress is undefined
        if (!Validators.isEmail(this._emailAddress.value)) {
            this.setState({ errors: 'Please enter an email address.' });
        } else {
            this.setState({ errors: 'All Good.' });
        }
    }
}

const TextInput = ({ label, inputName, ariaLabel, validation, ref }: { label: string; inputName: string; ariaLabel: string; validation?: string; ref: (ref: HTMLInputElement) => void }) => (
    <div>
        <label htmlFor='txt_register_first_name'>
            { label }
        </label>

        <input type='text' id={inputName} name={inputName} className='input ' aria-label={ariaLabel} ref={ref} />

        <div className='input_validation'>
            <span>{validation}</span>
        </div>
    </div>
);
bx2
  • 6,356
  • 5
  • 36
  • 40
drewwyatt
  • 5,989
  • 15
  • 60
  • 106

5 Answers5

149

You can useuseRef hook which is available since v16.7.0-alpha.

EDIT: You're encouraged to use Hooks in production as of 16.8.0 release!

Hooks enable you to maintain state and handle side effects in functional components.

function TextInputWithFocusButton() {
  const inputEl = useRef(null);
  const onButtonClick = () => {
    // `current` points to the mounted text input element
    inputEl.current.focus();
  };
  return (
    <>
      <input ref={inputEl} type="text" />
      <button onClick={onButtonClick}>Focus the input</button>
    </>
  );
}

Read more in Hooks API documentation

Ante Gulin
  • 2,022
  • 2
  • 15
  • 16
  • 3
    @Jafarrezaei Yes Hooks are in alpha at the moment but will most certainly be adopted. – Ante Gulin Jan 12 '19 at 17:45
  • 1
    @AnteGulin yes it will be adopted, but it's ALPHA. That means it's possibly unstable, with no guarantee that the current API will be what makes it to the final product. It exists purely for testing and feedback, NOT for production use. – craigmiller160 Jan 17 '19 at 21:08
  • 6
    It's out of alpha and even in react native now 7u7 – RegularGuy Mar 16 '19 at 17:57
69

EDIT: You now can with React Hooks. See the answer by Ante Gulin.

You can't access React like methods (like componentDidMount, componentWillReceiveProps, etc) on stateless components, including refs. Checkout this discussion on GH for the full convo.

The idea of stateless is that there isn't an instance created for it (state). As such, you can't attach a ref, since there's no state to attach the ref to.

Your best bet would be to pass in a callback for when the component changes and then assign that text to the parent's state.

Or, you can forego the stateless component altogether and use an normal class component.

From the docs...

You may not use the ref attribute on functional components because they don't have instances. You can, however, use the ref attribute inside the render function of a functional component.

function CustomTextInput(props) {
  // textInput must be declared here so the ref callback can refer to it
  let textInput = null;

  function handleClick() {
    textInput.focus();
  }

  return (
    <div>
      <input
        type="text"
        ref={(input) => { textInput = input; }} />
      <input
        type="button"
        value="Focus the text input"
        onClick={handleClick}
      />
    </div>
  );  
}
ThdK
  • 9,916
  • 23
  • 74
  • 101
BradByte
  • 11,015
  • 2
  • 37
  • 41
  • @jasan are you wrapping your input in another component? if so, you'll need to expose the focus method on that class. If you're still having an issue inspect the value of textInput in when you're calling focus. – BradByte Jun 16 '17 at 09:03
  • yes, I am using Field from redux-form. I found the solution. cheers – jasan Jun 16 '17 at 09:11
  • 1
    Well you can still access the ref by passing the value from the ref to the parent, through a prop function, at the stateless component do `ref={input => innerRef(input)}` , on the parent use the prop you passed like you use ref normally; `innerRef={input => this.customInputRef = input}` – Eyo Okon Eyo Apr 20 '18 at 11:55
  • 1
    this answer is obsolete since `v16.8.0` – Sebastian Scholle May 20 '19 at 09:12
  • how about when I wan't to use the textInput as soon as the page is loaded? how do I do the componentDidMount in functional component? I did use `` useEffect(x=>{ //use here now textInput; but it is still null... },[])`` – Suisse Jul 22 '21 at 20:29
7

This is late but I found this solution much better. Pay attention to how it uses useRef & how properties are available under current property.

function CustomTextInput(props) {
  // textInput must be declared here so the ref can refer to it
  const textInput = useRef(null);

  function handleClick() {
    textInput.current.focus();
  }

  return (
    <div>
      <input
        type="text"
        ref={textInput} />
      <input
        type="button"
        value="Focus the text input"
        onClick={handleClick}
      />
    </div>
  );
}

For more reference check react docs

Arun kumar
  • 1,041
  • 5
  • 20
  • 39
2

The value of your TextInput is nothing more than a state of your component. So instead of fetching the current value with a reference (bad idea in general, as far as I know) you could fetch the current state.

In a reduced version (without typing):

class Form extends React.Component {
  constructor() {
    this.state = { _emailAddress: '' };

    this.updateEmailAddress = this.updateEmailAddress.bind(this);
    this.handleSubmit = this.handleSubmit.bind(this);
  }

  updateEmailAddress(e) {
    this.setState({ _emailAddress: e.target.value });
  }

  handleSubmit() {
    console.log(this.state._emailAddress);
  }

  render() {
    return (
      <form onSubmit={this.handleSubmit}>
        <input
          value={this.state._emailAddress}
          onChange={this.updateEmailAddress}
        />
      </form>
    );
  }
}
  • 1
    Assigning to state gets a little rough when forms get lengthy, which is why I was trying to solve this with the `ref`. I totally buy that what I'm trying to do is not possible here, but would you mind explaining, or referencing an article that explains why using `ref` is a bad idea in general? – drewwyatt Dec 08 '16 at 21:47
  • Maybe you could compose your component in multiple levels? Try this question: https://stackoverflow.com/questions/29503213/use-state-or-refs-in-react-js-form-components –  Dec 08 '16 at 21:50
  • 1
    There's a word of caution here: https://facebook.github.io/react/docs/refs-and-the-dom.html#dont-overuse-refs – BradByte Dec 08 '16 at 22:05
1

You can also get refs into functional components with a little plumbing

import React, { useEffect, useRef } from 'react';

// Main functional, complex component
const Canvas = (props) => {
  const canvasRef = useRef(null);

    // Canvas State
  const [canvasState, setCanvasState] = useState({
      stage: null,
      layer: null,
      context: null,
      canvas: null,
      image: null
  });

  useEffect(() => {
    canvasRef.current = canvasState;
    props.getRef(canvasRef);
  }, [canvasState]);


  // Initialize canvas
  useEffect(() => {
    setupCanvas();
  }, []);

  // ... I'm using this for a Konva canvas with external controls ...

  return (<div>...</div>);
}

// Toolbar which can do things to the canvas
const Toolbar = (props) => {
  console.log("Toolbar", props.canvasRef)

  // ...
}

// Parent which collects the ref from Canvas and passes to Toolbar
const CanvasView = (props) => {
  const canvasRef = useRef(null);

  return (
    <Toolbar canvasRef={canvasRef} />
    <Canvas getRef={ ref => canvasRef.current = ref.current } />
}
verdverm
  • 329
  • 4
  • 11