React useImperativeHandle Hook

React useImperativeHandle Hook

In React, Data is passed from ( parent to child ) components via props, known as unidirectional data flow. The parent component cannot directly call a function defined in the child component or reach down to grab a value for itself.

With the useImperativeHandle and forwardRef, you can expose functionalities from a React Component (child) to its parent Component to then use your Component in the parent Component through refs and trigger certain functionalities.

In certain circumstances, we want our parent component to reach down to the child component, getting data that originates in the child component for its own use. We can achieve this type of data flow with the useImperativeHandle Hook, which allows us to expose a value, state, or function inside a child component to the parent component through ref. You can also decide which properties the parent component can access, thereby maintaining the private scoping of the child component.

Syntax:

useImperativeHandle(ref, createHandle, [dependencies])
  • ref: the ref passed down from the parent component

  • createHandle: the value to be exposed to the parent component

  • dependencies: an array of values that causes the Hook to rerun

  • when changed

Use cases

When you need a bidirectional data and logic flow, but you don’t want to overcomplicate things by introducing state management libraries, the useImperativeHandle Hook is a great choice.

For example, I once used the useImperativeHandle Hook when I needed to open a modal component when a button in the parent component was clicked.

Defining state in the parent component would cause the parent component and its children to re-render each time the modal button was clicked, therefore, I wanted the state in the child component. Instead, I stored the modal state in the Modal component using useImperativeHandle and forwardRef.

Consider the code below:

import React, { useRef } from 'react';
import Child from './Child';

 const ParentComponent = () => {

    const childRef = useRef(null);

    const handleOpenModal = (value) => {
        childRef.current.openModal(value);
    }


    return (
        <div>
            <p>This is a parent component</p>
            <Child ref={childRef}/>

            <button onClick={() => handleOpenModal(true)}>Open modal</button>
        </div>
    )

}

export default ParentComponent;

*Above, we defined a ref, which we passed down to the child component. In our code below, the ref will be the first parameter passed to useImperativeHandle in the child component.

We also defined a handleOpenModal function, which returns the openModal function passed up from the child component with childRef.current.openModal(value). The function is then called when the button is clicked.

The child component should look like the code below:*

import React, { forwardRef, useImperativeHandle, useState } from 'react';

function ChildComponent (props, ref) {
 const [openModal, setOpenModal] = useState(false);

 useImperativeHandle(ref, () => ({
  openModal: (value) => setOpenModal(value),
 }));

  if(!openModal) return null;

    return (
        <div>
            <p>This is a modal!</p>
            <button onClick={() => setOpenModal(false)}> Close </button>
        </div>
    )

}

export default forwardRef(ChildComponent);

We wrapped the child component in a forwardRef to expose the openModal function defined in useImperativeHandle. In the parent component, the state defined in the child component is changed, causing a re-render of only the child component. Problem solved!

example 2 : Parent Component

import React, { useState, useEffect, useReducer, useContext, useRef } from 'react';

import Card from '../UI/Card/Card';
import classes from './Login.module.css';
import Button from '../UI/Button/Button';
import AuthContext from '../../store/auth-context';
import Input from '../UI/Input/Input';



// function reducer 1
const emailReducer = (state, action) => {
  if (action.type === 'USER_INPUT') {
    return { value: action.val, isValid: action.val.includes('@') };

  }

  if (action.type === 'INPUT_BLUR') {
    return { value: state.value, isValid: state.value.includes('@') };
  }

  return { value: '', isValid: false };
}



////////////////////////////////////////
//function reducer 2
const passwordReducer = (state, action) => {
  if (action.type === 'USER_INPUT') {
    return { value: action.val, isValid: action.val.trim().length > 6 };

  }

  if (action.type === 'INPUT_BLUR') {
    return { value: state.value, isValid: state.value.trim().length > 6 };
  }

  return { value: '', isValid: false };
}

//////////////////////////////////////////////////////////

const Login = (props) => {


  const [formIsValid, setFormIsValid] = useState(false);


  //btebda intial state hik
  const [emailState, dispatchEmail] = useReducer(emailReducer, {
    value: '',
    isValid: null,
  });



  const [passwordState, dispatchPassword] = useReducer(passwordReducer, {
    value: '',
    isValid: null,
  });

  /////////////////////////////////////

  const authCtx = useContext(AuthContext);

  const emailInputRef = useRef();
  const passwordInputRef = useRef();






  //object descructuring syntax
  const { isValid: emailIsValid } = emailState;
  const { isValid: passwordIsValid } = passwordState;

  //const isValid = emailState.isValid;










  useEffect(() => {
    const identifier = setTimeout(() => {
      console.log("checking form validity!"); //3 first , second    //3
      setFormIsValid(
        emailIsValid && passwordIsValid
      );
    }, 1000);

    return () => {
      console.log('CLEANUP');//2  fisrt  , second    //1
      clearTimeout(identifier);
    };

  }, [emailIsValid, passwordIsValid]);







  // lama tektob
  const emailChangeHandler = (event) => {
    dispatchEmail({ type: 'USER_INPUT', val: event.target.value });

  };




  //small change
  const passwordChangeHandler = (event) => {
    dispatchPassword({ type: 'USER_INPUT', val: event.target.value });

  };



  //change
  const validateEmailHandler = () => {
    dispatchEmail({ type: 'INPUT_BLUR' });
  };



  //nothing change
  const validatePasswordHandler = () => {
    dispatchPassword({ type: 'INPUT_BLUR' });
  };


  //emailState.value
  const submitHandler = (event) => {
    event.preventDefault();

    if (formIsValid) {
      authCtx.onLogin(emailState.value, passwordState.value);
    } else if (!emailIsValid) {
      emailInputRef.current.focus();
    } else {
      passwordInputRef.current.focus();
    }
  };




  return (
    <Card className={classes.login}>
      <form onSubmit={submitHandler}>
        <Input
          ref={emailInputRef}
          id="email"
          label="E-Mail"
          type="email"
          isValid={emailIsValid}
          value={emailState.value}
          onChange={emailChangeHandler}
          onBlur={validateEmailHandler} />

        <Input
          ref={passwordInputRef}
          id="password"
          label="Password"
          type="password"
          isValid={passwordIsValid}
          value={passwordState.value}
          onChange={passwordChangeHandler}
          onBlur={validatePasswordHandler} />

        <div className={classes.actions}>
          <Button type="submit" className={classes.btn} >Login</Button>
        </div>
      </form>
    </Card>
  );
};

export default Login;

Child Component

import React, { useRef, useEffect, useImperativeHandle } from 'react'
import classes from './Input.module.css'
const Input = React.forwardRef((props, ref) => {

    //ref hook
    const inputRef = useRef();

    //method
    const activate = () => {
        inputRef.current.focus();
    };


    useImperativeHandle(ref, () => {
        return {
            focus: activate,
        };
    });


    return (
        <div className={`${classes.control} ${props.isValid === false ? classes.invalid : ''}`} >
            <label htmlFor={props.id}>{props.label}</label>
            <input
                ref={inputRef}
                type={props.type}
                id={props.id}
                value={props.value}
                onChange={props.onChange}
                onBlur={props.onBlur}
            />
        </div>
    )
});


export default Input;