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

3

5

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.
Dauphin answered 2/3, 2021 at 19:4 Comment(1)
You can pass a method to a child as a prop, which the child will call passing its validation method as a callback, the parent will receive the callback, save it to a ref and call when needed? (Hope it makes any sense :D )Cocoon
C
12

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/

Cocoon answered 2/3, 2021 at 19:22 Comment(3)
Investgating this code snippet and flow, I think I found where my issue was. In the case above, when calling the validate function, the function was making a callback to the parent with the valid data. However, it looks like ordering of this is that the callback will not actually be called on the parent until after the validate function is fully executed. This is the reason my code was requiring two clicks to actually go through. Will mark this as accepted as I am going with this method!Dauphin
Glad it worked for you! You can remove onValidChange and just return the result value from verify on submit (unless of course the parent needs to be aware of the current state before submit)Cocoon
That's actually just what I finished refactoring it to do! Thanks again.Dauphin
D
1

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.

Dauphin answered 10/3, 2021 at 4:42 Comment(0)
D
1

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>
        )
    }
Daiseydaisi answered 13/5, 2022 at 18:34 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.