Как вызвать метод дочернего компонента из родительского компонента с помощью useImperativeHandle

год назад·3 мин. на чтение

Быстрый старт с useImperativeHandle

В этой статье будет показано, как вызвать метод дочернего компонента с помощью ссылки. Чтобы решить эту проблему, мы будем использовать хуки useRef и useImperativeHandle.

Дочерний компонент

Начнем с простого дочернего компонента, в котором содержится кнопка. Нажатие на кнопку вызывает внутренний метод doSomething.
// Child.jsx

function Child(props, ref) {
  const doSomething = () => {
    console.log("do something");
  };
  return (
    <div>
      <h1>Child Component</h1>
      <button onClick={doSomething}>Run</button>
    </div>
  );
}

export default Child;

Родительский компонент

Далее рассмотрим родительский компонент. В нем используется дочерний компонент, описанный выше. Обратите внимание, что в родительском компоненте есть собственная кнопка сохранения.
// App.jsx

import Child from "./Child";

function App() {
  const save = () => {};
  return (
    <div>
      <Child />
      <button onClick={save}>Save</button>
    </div>
  );
}

export default App;

Хук useImperativeHandle

Теперь давайте сосредоточимся на нашей задаче. Мы хотим вызвать метод (doSomething) дочернего компонента при нажатии кнопки (Save) из родительского компонента. Чтобы вызвать метод из дочернего компонента, нам нужно сначала выставить его наружу. useImperativeHandle определяет значение объекта, которое предоставляется родительскому компоненту при использовании ref. Добавляя наш метод к этому объекту, мы делаем его доступным в родительских компонентах.
// Child.jsx

import { useImperativeHandle } from "react";

function Child(props, ref) {
  const doSomething = () => {
    console.log("do something");
  };

  useImperativeHandle(ref, () => ({ doSomething }));

  return (
    <div>
      <h1>Child Component</h1>
      <button onClick={doSomething}>Run</button>
    </div>
  );
}
export default Child;
useImperativeHandle следует использовать с forwardRef.
forwardRef позволяет родительскому компоненту передавать ссылки своим дочерним элементам. Чтобы прикрепить функции или поля к этой ссылке (к рефу), используется хук useImperativeHandle.
// Child.jsx

import { forwardRef, useImperativeHandle } from "react";

function Child(props, ref) {
  const doSomething = () => {
    console.log("do something");
  };

  useImperativeHandle(ref, () => ({ doSomething }));

  return (
    <div>
      <h1>Child Component</h1>
      <button onClick={doSomething}>Run</button>
    </div>
  );
}

export default forwardRef(Child); // Child обернут в forwardRef
На этом этапе мы можем создать ссылку в родительском компоненте с помощью хука useRef и передать ее дочернему компоненту. Получив эту ссылку, мы можем вызвать метод doSomething дочернего компонента.
// App.jsx

import { useRef } from "react";
import Child from "./Child";

function App() {
  const childRef = useRef(null);

  const save = () => {
    if (childRef.current) {
      childRef.current.doSomething();
    }
  };

  return (
    <div>
      <Child ref={childRef} />
      <button onClick={save}>Save</button>
    </div>
  );
}

export default App;

Добавим TypeScript

Далее посмотрим, какие изменения нужно сделать, чтобы вызвать тот же дочерний метод из родительского компонента при использовании TypeScript. Во-первых, нам нужно определить новый интерфейс, содержащий метод, который будет представлен.
export interface RefType {
  doSomething: () => void;
}
Затем новый тип (RefType) используется при получении ссылки в дочернем компоненте.
function Child(props: PropsType, ref: Ref<RefType>)
Ниже приведен полный код дочернего компонента.
// Child.jsx

import { forwardRef, useImperativeHandle, Ref } from "react";

export interface PropsType {}
export interface RefType {
  doSomething: () => void;
}

function Child(props: PropsType, ref: Ref<RefType>) {
  const doSomething = () => {
    console.log("do something");
  };
  useImperativeHandle(ref, () => ({ doSomething }));
  return (
    <div>
      <h1>Child Component</h1>
      <button onClick={doSomething}>Run</button>
    </div>
  );
}

export default forwardRef(Child);
В родительский компонент нам нужно импортировать этот RefType, содержащий все публичные дочерние методы, и использовать его при создании ref.
// App.jsx

import Child, { RefType } from "./Child";
//...
const childRef = useRef<RefType>(null);
Полный код родительского компонента.
import { useRef } from "react";
import Child, { RefType } from "./Child";

function App() {
  const childRef = useRef<RefType>(null);

  const save = () => {
    if (childRef.current) {
      childRef.current.doSomething();
    }
  };

  return (
    <div>
      <Child ref={childRef} />
      <button onClick={save}>Save</button>
    </div>
  );
}

export default App;

Кастомный хук для диспатчинга Redux экшенов

год назад·1 мин. на чтение

В этой статье напишем кастомный хук для вызова action creator’а Redux вместе с dispatch() внутри.

Часто, вызывая action creator, мы забываем обернуть его в dispatch(), а вместо этого вызываем как обычная функцию. Таким образом action не доходит до редьюсера.
addProductToCart(product); // не верно
dispatch(addProductToCart(product)); // верно
Мы создадим собственный хук, в котором будет выполняться обертывание вызова функции в dispatch(), чтобы мы могли отправлять наши экшены, вызывая их как обычные функции с уже встроенным диспатчингом.
import { useCallback } from 'react';
import { useDispatch } from 'react-redux';

export const useWithDispatch = (fn) => {
  const dispatch = useDispatch();

  return useCallback(
    payload => () => dispatch(fn(payload)),
    [dispatch, fn]
  ) 
}
Реализация довольно проста. useWithDispatch — это функция высшего порядка, поскольку она принимает функцию (action creator) в качестве аргумента и возвращает другую функцию, которая при вызове вызовет переданную функцию, обернутую с помощью dispatch(). Обязательно запоминаем функцию обратного вызова с помощью useCallback.
Допустим, мы работаем над приложением списка задач, и у нас есть действие addTask(), которое берет текст задачи и добавляет его в список задач:
// actions.js

const addTaskAction = (text) => {
  return {
    type: 'ADD_TASK',
    payload: {
      id: 'some-id',
      text
    }
  }
}
В компоненте вызов будет выглядеть следующим образом.
import { addTaskAction } from './actions';

export const Component = () => {
  const addTask = useWithDispatch(addTaskAction);

  const handleClick = () => {
    addTask('Learn Redux');
  }

  // ...
}

Итоги

Я надеюсь, что вы нашли этот пост полезным. Вам по-прежнему нужно помнить о передаче создателей действий (action creator) в useWithDispatch, но будет легче запомнить, что вы делаете это в самом начале, а не при их вызове.