How does one trigger an action in a child functional component in React?

Viewed 1627

With a basic form/input layout, it's clear that a callback should be used for state changes from child to parent (initiated by child), but how can the parent ask the child component to re-assess its state and communicate that back to parent?

The end goal here is simply to trigger validation of child inputs upon submit of a form button.

Given [ts] code that looks like this:

    const Login : React.FC<Props> = (props) => {
        ...useStates omitted

        const onSubmit = () : void => {
          //trigger `verify()` in PasswordInput to get up-to-date `valid` state var

 
        }
        
        return (
            <PasswordInput
              onValidChange={setValid} />
            <Button
              onPress={submit} />
        )
    }


    const PasswordInput : React.FC<Props> = (props) => {
        ...useStates omitted

        const verify = () => {
          //verify the password value from state

          props.onValidChange(true)
        }


        return (<Input onBlur={verify}/>) 
    }

Notes/paths taken thus far:

UPDATE Lessons learned:

  • If you are going to trigger an action in a child component, you may use the refs method outlined by Nadia below, but the more proper React Way® is probably via a shared Reducer.
  • Don't expect state to always be updated through callbacks to your parent at time of calling into said reference. In my case, the only method ordering that worked was to have what would be the verify method above actually return the up-to-date values.
3 Answers

A simple example of how you can approach this

function Child(props)
{
    const validate=()=> alert('hi from the child');
    props.registerCallback(validate)
    return (<div>I'm the child</div>)
}

function Parent()
{
    const callbackRef = React.useRef();
    function registerCallback(callback)
    {
        callbackRef.current = callback;
    }
    return (<div><Child  registerCallback={registerCallback}/>
        <button onClick={() => callbackRef.current()}>say hello</button></div>)
}

https://jsfiddle.net/4howanL2/5/

After some more learning about React and a few iterations of a working solution, I settled on using a Reducer to accomplish this task.

Instead of looking at child components as a function to be called, I had to instead switch my thinking to be more around just updating state and trusting the child components to represent that state correctly. For my original example, I ended up structuring things similar to this (everything simplified down and trimmed):

interface LoginState {
    email: { 
      status: 'none' | 'verify' | 'valid', 
      value?: string
    }
    password: { 
      status: 'none' | 'verify' | 'valid', 
      value?: string
    }
    submit: boolean
}
const Login : React.FC<Props> = (props) => {

        const [state, dispatch] = useReducer<Reducer>(reducer, {
            email: { status: 'none' },
            password: { status: 'none' }}
        })

        export const reducer = (state : LoginState, change : StateChange) : LoginState => {

            if (change.email) {
                state.email = _.merge({}, state.email, change.email)
            }

            if (change.password) {
                state.password = _.merge({}, state.password, change.password)
            }

            return _.merge({}, state)
        }

        const submit = () : void => {
            dispatch({
                email: { status: 'verify' }},
                password: { status: 'verify'}}},
                submit: true
            })
 
        }

        useEffect(() => {
            if (!state.submit 
                || state.email.status == 'none' 
                || state.password.satus == 'none') {
                return
            }

            //ready to submit, abort if not valid
            if (state.email.status == 'invalid'
                || state.password.status == 'invalid') {
                dispatch({ submit: false})
                return
            }

           //all is valid after this point
        }, [state.email, state.password])
        
        return (
            <Input ...
            <PasswordInput
              state={state.password}
              dispatch={dispatch} />
            <Button
              onPress={submit} />
        )
    }


    const PasswordInput : React.FC<Props> = (props) => {

        //setup onChangeText to dispatch to update value

        return (<Input
                  //do something visual with props.state.status
                  value={props.state.value}
                  onBlur={dispatch({ status: 'verify'})} ... />) 
    }

The above is rough code, but the gist is there. This communicates updating state of the underlying values, which are reduced up at a central level. This state is then rendered back down at the child component level by telling the inputs that they are in state verify. When submit is set to true, we try to submit the form, validating in the process.

I accomplished something similar using an EventEmitter. It's similar to the registerCallback approach already described, but imho it's a little bit cleaner.

    function Child({eventEmitter})
    {
        eventEmitter.on('sayHello', () => alert('hi from the child'))
        return (<div>I'm the child</div>)
    }

    function Parent()
    {
        const eventEmitter = new EventEmitter()
        return (
            <div>
                <Child eventEmitter={eventEmitter}/>
                <button onClick={() => eventEmitter.emit('sayHello')}>
                    say hello
                </button>
            </div>
        )
    }
Related