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.
Zack
  • 1,181
  • 2
  • 11
  • 26
  • 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 ) – Nadia Chibrikova Mar 02 '21 at 19:10

3 Answers3

9

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/

desertSniper87
  • 795
  • 2
  • 13
  • 25
Nadia Chibrikova
  • 4,916
  • 1
  • 15
  • 17
  • 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! – Zack Mar 02 '21 at 20:56
  • 1
    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) – Nadia Chibrikova Mar 02 '21 at 21:06
  • 1
    That's actually just what I finished refactoring it to do! Thanks again. – Zack Mar 02 '21 at 21:31
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.

Zack
  • 1,181
  • 2
  • 11
  • 26
0

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>
        )
    }
nbrustein
  • 727
  • 5
  • 16