useEffect вы действительно знаете, как его использовать?

React.js

57a4d726ffd0f1c540e857f3660fff08.jpeg

Недавно столкнулся с багом в процессе перемещения кирпичей в компании.При загрузке страницы она какое-то время мигает.Понадобилось много времени, чтобы выяснить, что это проблема с зависимостями useeffect, поэтому я планирую написать статью по итогу.помогло.

1. Что такое useEffect?

Хук принимает функцию, содержащую императивный, возможно, побочный код.
Изменение DOM, добавление подписок, установка таймеров, ведение журнала и выполнение других операций с побочными эффектами не разрешены в теле функционального компонента (в данном случае на этапе рендеринга React), так как это может создать необъяснимые ошибки и нарушить Согласованность пользовательского интерфейса. Используйте useEffect для выполнения операций с побочными эффектами. Функция, назначенная для useEffect, будет выполнена после того, как компонент отобразится на экране. Вы можете думать об эффектах как о пути выхода из чисто функционального мира React в императивный мир. (официальная документация)

Вы можете быть немного сбиты с толку этим...
Рассмотрим следующий пример:

import React, { useState, useEffect } from 'react';

function Example() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    document.title = `You clicked ${count} times`;
  });

  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>
        Click me
      </button>
    </div>
  );
}

Что делает useEffect?
Используя этот хук, вы можете сказать компоненту React, что ему нужно что-то сделать после рендеринга. React сохраняет переданную вами функцию (назовем ее «эффектом») и вызывает ее после выполнения обновления DOM. В этом случае мы устанавливаем свойство заголовка документа, но мы также можем выполнять выборку данных или вызывать другие императивные API.

Зачем вызывать useEffect внутри компонента?
Размещение useEffect внутри компонента дает нам прямой доступ к переменной состояния count (или другим реквизитам) в эффекте. Нам не нужен специальный API для его чтения — он уже хранится в области видимости функции. Хуки используют механизм закрытия JavaScript вместо того, чтобы вводить определенные API React, когда JavaScript уже предоставляет решение.

Будет ли useEffect выполняться после каждого рендера?
Да, по умолчанию он выполняется после первого рендера и после каждого обновления. (Мы поговорим о том, как управлять этим позже.) Вам, вероятно, будет удобнее, если эффект произойдет «после рендеринга», и вам не придется беспокоиться о «монтировании» или «обновлении». React гарантирует, что при каждом запуске эффекта DOM обновляется. Если вы знакомы с функциями жизненного цикла классов React, вы можете рассматривать useEffect Hook как комбинацию трех функций: componentDidMount, componentDidUpdate и componentWillUnmount.

2. Как использовать useEffect

2.1 Реализовать функцию componentDidMount

Второй параметр useEffect — это пустой массив, который не будет выполняться после вызова инициализации, что эквивалентно componentDidMount.

function Demo () {
  useEffect(() => {
    console.log('hello world')
  }, [])
  return (
    <div>
      hello world
    </div>
  )
}
// 等价于
class Demo extends Component {
  componentDidMount() {
    console.log('hello world')
  }
  render() {
    return (
      <div>
        hello world
      </div>
    );
  }
}

2.2 Реализовать функцию объединения componentDidMount с componentDidUpdate

Когда useEffect не имеет второго параметра, выполняются как инициализация, так и обновление компонента.

class Example extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      count: 0
    };
  }

  componentDidMount() {
    document.title = `You clicked ${this.state.count} times`;
  }

  componentDidUpdate() {
    document.title = `You clicked ${this.state.count} times`;
  }

  render() {
    return (
      <div>
        <p>You clicked {this.state.count} times</p>
        <button onClick={() => this.setState({ count: this.state.count + 1 })}>
          Click me
        </button>
      </div>
    );
  }
}
// 等价于
import React, { useState, useEffect } from 'react';

function Example() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    document.title = `You clicked ${count} times`;
  });

  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>
        Click me
      </button>
    </div>
  );
}

2.3 Реализовать функцию объединения componentDidMount с componentWillUnmount

useEffect возвращает функцию, которая будет выполняться при размонтировании компонента.

class Example extends Component {
  constructor (props) {
    super(props);
    this.state = {
      count: 0
    }
  }
  componentDidMount() {
    this.id = setInterval(() => {
      this.setState({count: this.state.count + 1})
    }, 1000);
  }
  componentWillUnmount() {
    clearInterval(this.id)
  }
  render() { 
    return <h1>{this.state.count}</h1>;
  }
}
// 等价于
function Example() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    const id = setInterval(() => {
      setCount(c => c + 1);
    }, 1000);
    return () => clearInterval(id);
  }, []);

  return <h1>hello world</h1>
}

3. Яма, используемая useEffect

3.1 Бесконечный цикл

Когда второй параметр useEffect передает массив для передачи зависимости, когда значение зависимости изменяется, это инициирует выполнение useEffect.
См. пример ниже:

Компонент приложения отображает список элементов, а функции состояния и обновления состояния поступают из хука useState, который создает внутреннее состояние компонента приложения путем вызова useState. Исходное состояние — это объект, в котором hits — это пустой массив, а интерфейса для запроса к бэкенду пока нет.

import React, { useState } from 'react';
 
function App() {
  const [data, setData] = useState({ hits: [] });
 
  return (
    <ul>
      {data.hits.map(item => (
        <li key={item.objectID}>
          <a href={item.url}>{item.title}</a>
        </li>
      ))}
    </ul>
  );
}
 
export default App;

Чтобы получить данные, предоставленные серверной частью, для инициации запроса будет использоваться axios, а также может использоваться fetch, где useEffect будет использоваться для изоляции побочных эффектов.

import React, { useState, useEffect } from 'react';
import axios from 'axios';
 
function App() {
  const [data, setData] = useState({ hits: [] });
 
  useEffect(async () => {
    const result = await axios(
      'http://localhost/api/v1/search?query=redux',
    );
 
    setData(result.data);
  });
 
  return (
    <ul>
      {data.hits.map(item => (
        <li key={item.objectID}>
          <a href={item.url}>{item.title}</a>
        </li>
      ))}
    </ul>
  );
}
 
export default App;

В useEffect запрашиваются не только данные бэкэнда, но и локальное состояние обновляется вызовом setData, что инициирует обновление представления.

Однако при запуске этой программы возникает бесконечный цикл. useEffect выполняется при монтировании компонента, а также при его обновлении. Поскольку мы устанавливаем локальное состояние после каждого запроса данных, компонент обновляется, поэтому useEffect выполняется снова, поэтому возникает бесконечный цикл. Мы хотим запрашивать данные только тогда, когда компонент смонтирован. Мы можем передать пустой массив в качестве второго параметра useEffect, что позволяет избежать выполнения useEffect при обновлении компонента и выполняет его только при монтировании компонента.

import React, { useState, useEffect } from 'react';
import axios from 'axios';
 
function App() {
  const [data, setData] = useState({ hits: [] });
 
  useEffect(async () => {
    const result = await axios(
      'http://localhost/api/v1/search?query=redux',
    );
 
    setData(result.data);
  }, []);
 
  return (
    <ul>
      {data.hits.map(item => (
        <li key={item.objectID}>
          <a href={item.url}>{item.title}</a>
        </li>
      ))}
    </ul>
  );
}
 
export default App;

Второй параметр useEffect может использоваться для определения всех переменных, от которых он зависит. Если одна из переменных изменяется, useEffect запускается снова. Если массив, содержащий переменные, пуст, useEffect больше не будет выполняться при обновлении компонента, так как он не будет прослушивать какие-либо изменения переменных.

Посмотрите еще раз на этот пример:
Бизнес-сценарий: вам нужно получить возвращаемое значение интерфейса в начале страницы и вызвать другой интерфейс.
Моя идея состоит в том, чтобы сначала установить возвращаемое значение этого интерфейса в data=[] и подождать, пока данные не будут запрашивать другой интерфейс, то есть данные будут переданы в качестве второго параметра useEffect.
Но я не знаю, почему это вызовет бесконечный цикл, и мы не сможем получить желаемые результаты.
Пока я не увидел этот пример на официальном сайте:

1271680-20190725102526579-480067022.png

useEffect сравнит значение предыдущего рендеринга и следующего рендеринга, изменит начальное значение данных на неопределенное, и все заработает.

3.2 Ошибки при использовании асинхронного ожидания

В коде мы используем async/await для получения данных из стороннего API. Если вы знакомы с async/await, то знаете, что каждая асинхронная функция по умолчанию возвращает неявное обещание. Однако useEffect не должен ничего возвращать. Вот почему вы видите следующее предупреждение в журнале консоли:

Предупреждение: функция useEffect должна возвращать функцию очистки или ничего. Promises и useEffect(async() => …) не поддерживаются, но вы можете вызвать асинхронную функцию внутри эффекта

Вот почему вы не можете использовать асинхронные функции непосредственно в useEffect, поэтому вместо прямого вызова асинхронных функций мы можем сделать что-то вроде этого:

function App() {
  const [data, setData] = useState({ hits: [] });
 
  useEffect(() => {
    const fetchData = async () => {
      const result = await axios(
        'http://localhost/api/v1/search?query=redux',
      );
 
      setData(result.data);
    };
 
    fetchData();
  }, []);
 
  return (
    <ul>
      {data.hits.map(item => (
        <li key={item.objectID}>
          <a href={item.url}>{item.title}</a>
        </li>
      ))}
    </ul>
  );
}

4. Применение useEffect в реальном бою

4.1 Реагирование на обновления

Во многих случаях нам нужно ответить на ввод пользователя, прежде чем запрашивать его. На данный момент мы представим поле ввода для отслеживания изменения значения запроса:

import axios from 'axios';
 
function App() {
  const [data, setData] = useState({ hits: [] });
  const [query, setQuery] = useState('redux');
 
  useEffect(() => {
    const fetchData = async () => {
      const result = await axios(
        'http://localhost/api/v1/search?query=redux',
      );
      setData(result.data);
    };
 
    fetchData();
  }, []);
 
  return (
    <Fragment>
      <input
        type="text"
        value={query}
        onChange={event => setQuery(event.target.value)}
      />
      <ul>
        {data.hits.map(item => (
          <li key={item.objectID}>
            <a href={item.url}>{item.title}</a>
          </li>
        ))}
      </ul>
    </Fragment>
  );
}

Есть значение запроса, обновлена ​​логика запроса, и значение запроса нужно передать в фон, эта операция будет выполняться в useEffect
Как мы уже говорили ранее, текущий useEffect будет выполняться только при монтировании компонента, а второй параметр useEffect является зависимой переменной.Как только зависимая переменная изменится, useEffect будет выполняться повторно, поэтому нам нужно добавить запрос как зависимость useEffect :

function App() {
  const [data, setData] = useState({ hits: [] });
  const [query, setQuery] = useState('redux');
 
  useEffect(() => {
    const fetchData = async () => {
      const result = await axios(
        `http://localhost/api/v1/search?query=${query}`,
      );
 
      setData(result.data);
    };
 
    fetchData();
  }, [query]);
 
  return (
    ...
  );
}

После изменения значения запроса данные можно снова получить. Но это принесет еще одну проблему: любое изменение запроса будет запрашивать серверную часть, что приведет к относительно большому давлению доступа. В это время нам нужно ввести кнопку, нажмите эту кнопку, чтобы инициировать запрос.

function App() {
 const [data, setData] = useState({ hits: [] });
 const [query, setQuery] = useState('redux');
 const [search, setSearch] = useState('');

 useEffect(() => {
   const fetchData = async () => {
     const result = await axios(
       `http://localhost/api/v1/search?query=${query}`,
     );

     setData(result.data);
   };

   fetchData();
 }, [query]);

 return (
   <Fragment>
     <input
       type="text"
       value={query}
       onChange={event => setQuery(event.target.value)}
     />
     <button type="button" onClick={() => setSearch(query)}>
       Search
     </button>

     <ul>
       {data.hits.map(item => (
         <li key={item.objectID}>
           <a href={item.url}>{item.title}</a>
         </li>
       ))}
     </ul>
   </Fragment>
 );
}

Вы можете видеть выше, что мы добавили новую кнопку, а затем создали новое состояние компонента: поиск. Каждый раз, когда нажимается кнопка, значение поиска будет установлено на запрос.На данный момент нам нужно изменить зависимость в useEffect для поиска, чтобы каждый раз, когда нажимается кнопка, значение поиска изменялось, а useEffect выполняться повторно, чтобы избежать ненужных изменений:

function App() {
  const [data, setData] = useState({ hits: [] });
  const [query, setQuery] = useState('redux');
  const [search, setSearch] = useState('redux');
 
  useEffect(() => {
    const fetchData = async () => {
      const result = await axios(
        `http://localhost/api/v1/search?query=${search}`,
      );
 
      setData(result.data);
    };
 
    fetchData();
  }, [search]);
 
  return (
    ...
  );
}
 
export default App;

Кроме того, начальное состояние состояния поиска устанавливается в то же состояние, что и состояние запроса, поскольку компонент сначала извлекает данные при монтировании. Итак, просто установите серверный URL-адрес, который будет запрашиваться, в качестве начального значения состояния поиска.

function App() {
  const [data, setData] = useState({ hits: [] });
  const [query, setQuery] = useState('redux');
  const [url, setUrl] = useState(
    'http://localhost/api/v1/search?query=redux',
  );
 
  useEffect(() => {
    const fetchData = async () => {
      const result = await axios(url);
 
      setData(result.data);
    };
 
    fetchData();
  }, [url]);
 
  return (
    <Fragment>
      <input
        type="text"
        value={query}
        onChange={event => setQuery(event.target.value)}
      />
      <button
        type="button"
        onClick={() =>
          setUrl(`http://localhost/api/v1/search?query=${query}`)
        }
      >
        Search
      </button>
    <ul>
        {data.hits.map(item => (
          <li key={item.objectID}>
            <a href={item.url}>{item.title}</a>
          </li>
        ))}
      </ul>
    </Fragment>
  );
}

4.2 Как справиться с загрузкой и ошибкой

Для хорошего взаимодействия с пользователем необходимо показывать состояние загрузки, когда запрашиваются серверные данные, а данные не были возвращены, поэтому нам также необходимо добавить состояние загрузки.

import React, { Fragment, useState, useEffect } from 'react';
import axios from 'axios';
 
function App() {
  const [data, setData] = useState({ hits: [] });
  const [query, setQuery] = useState('redux');
  const [url, setUrl] = useState(
    'http://hn.algolia.com/api/v1/search?query=redux',
  );
  const [isLoading, setIsLoading] = useState(false);
 
  useEffect(() => {
    const fetchData = async () => {
      setIsLoading(true);
 
      const result = await axios(url);
 
      setData(result.data);
      setIsLoading(false);
    };
 
    fetchData();
  }, [url]);
  return (
    <Fragment>
      <input
        type="text"
        value={query}
        onChange={event => setQuery(event.target.value)}
      />
      <button
        type="button"
        onClick={() =>
          setUrl(`http://localhost/api/v1/search?query=${query}`)
        }
      >
        Search
      </button>
 
      {isLoading ? (
        <div>Loading ...</div>
      ) : (
        <ul>
          {data.hits.map(item => (
            <li key={item.objectID}>
              <a href={item.url}>{item.title}</a>
            </li>
          ))}
        </ul>
      )}
    </Fragment>
  );
}

В useEffect установите для загрузки значение true перед запросом данных и установите для загрузки значение false после завершения запроса. Мы видим, что к данным зависимости useEffect не добавляется загрузка, потому что нам не нужно снова вызывать useEffect при изменении загрузки. Помните: только если вам нужно повторно выполнить useEffect после обновления переменной, вам нужно добавить переменную в массив зависимостей useEffect.

После того, как обработка загрузки завершена, вам также нужно обработать ошибки, логика здесь та же, используйте useState для создания нового состояния, а затем обновите состояние в определенной позиции в useEffect. Поскольку мы используем async/await, можно использовать большой try-catch:

import React, { Fragment, useState, useEffect } from 'react';
import axios from 'axios';
 
function App() {
  const [data, setData] = useState({ hits: [] });
  const [query, setQuery] = useState('redux');
  const [url, setUrl] = useState(
    'http://localhost/api/v1/search?query=redux',
  );
  const [isLoading, setIsLoading] = useState(false);
  const [isError, setIsError] = useState(false);
 
  useEffect(() => {
    const fetchData = async () => {
      setIsError(false);
      setIsLoading(true);
 
      try {
        const result = await axios(url);
 
        setData(result.data);
      } catch (error) {
        setIsError(true);
      }
 
      setIsLoading(false);
    };
 
    fetchData();
  }, [url]);
  return (......)

Ошибка сбрасывается каждый раз, когда выполняется useeffect; при возникновении ошибки для ошибки устанавливается значение true; после завершения обычного запроса для ошибки устанавливается значение FALSE.

4.3 Обработка формы

Обычно мы используем не только вышеперечисленные поля ввода и кнопки, но чаще это форму, поэтому мы также можем использовать useEffect в форме для обработки запросов данных, логика такая же:

function App() {
  ...
 
  return (
    <Fragment>
      <form
        onSubmit={() =>
          setUrl(`http://localhost/api/v1/search?query=${query}`)
        }
      >
        <input
          type="text"
          value={query}
          onChange={event => setQuery(event.target.value)}
        />
        <button type="submit">Search</button>
      </form>
 
      {isError && <div>Something went wrong ...</div>}
 
      ...
    </Fragment>
  );
}

В приведенном выше примере обновление страницы запускается при отправке формы; как обычно, событие по умолчанию необходимо заблокировать, чтобы предотвратить обновление страницы.

function App() {
  ...
 
  const doFetch = () => {
    setUrl(`http://localhost/api/v1/search?query=${query}`);
  };
 
  return (
    <Fragment>
      <form onSubmit={event => {
        doFetch();
 
        event.preventDefault();
      }}>
        <input
          type="text"
          value={query}
          onChange={event => setQuery(event.target.value)}
        />
        <button type="submit">Search</button>
      </form>
 
      {isError && <div>Something went wrong ...</div>}
 
      ...
    </Fragment>
  );
}

4.4 Пользовательские хуки

Мы видим, что вышеуказанный компонент после добавления ряда хуков и логики стал очень большим. Тогда как нам с этим справиться? Преимущество хуков в том, что они могут легко извлекать пользовательские хуки. На данный момент мы можем извлечь большую часть вышеописанной логики в отдельные хуки, что удобно для повторного использования и разделения.

function useHackerNewsApi = () => {
  const [data, setData] = useState({ hits: [] });
  const [url, setUrl] = useState(
    'http://localhost/api/v1/search?query=redux',
  );
  const [isLoading, setIsLoading] = useState(false);
  const [isError, setIsError] = useState(false);
 
  useEffect(() => {
    const fetchData = async () => {
      setIsError(false);
      setIsLoading(true);
 
      try {
        const result = await axios(url);
 
        setData(result.data);
      } catch (error) {
        setIsError(true);
      }
 
      setIsLoading(false);
    };
 
    fetchData();
  }, [url]);
 
  const doFetch = () => {
    setUrl(`http://localhost/api/v1/search?query=${query}`);
  };
 
  return { data, isLoading, isError, doFetch };
}

После извлечения пользовательских хуков они вводятся в компонент.

function App() {
  const [query, setQuery] = useState('redux');
  const { data, isLoading, isError, doFetch } = useHackerNewsApi();
 
  return (
    <Fragment>
      ...
    </Fragment>
  );
}

Затем нам нужно установить начальный URL-адрес бэкэнда в компоненте формы.

const useHackerNewsApi = () => {
  ...
 
  useEffect(
    ...
  );
 
  const doFetch = url => {
    setUrl(url);
  };
 
  return { data, isLoading, isError, doFetch };
};
 
function App() {
  const [query, setQuery] = useState('redux');
  const { data, isLoading, isError, doFetch } = useHackerNewsApi();
 
  return (
    <Fragment>
      <form
        onSubmit={event => {
          doFetch(
            `http://localhost/api/v1/search?query=${query}`,
          );
 
          event.preventDefault();
        }}
      >
        <input
          type="text"
          value={query}
          onChange={event => setQuery(event.target.value)}
        />
        <button type="submit">Search</button>
      </form>
 
      ...
    </Fragment>
  );
}

4.5 Интеграция логики с помощью useReducer

До сих пор мы использовали различные обработчики состояния для управления данными, включая такие состояния, как загрузка, ошибка, данные и т. д. Но мы видим, что эти три связанных состояния действительно разбросаны.Они создаются отдельными useStates.Чтобы интегрировать связанные состояния, нам нужно использовать useReducer.

Если вы написали редукцию, вы будете хорошо знакомы с useReducer, который можно понимать как облегченный редукс. useReducer возвращает объект состояния и функцию отправки, которая может изменить объект состояния. Подобно редуксу, функция отправки принимает действие в качестве параметра, а действие содержит свойства типа и полезной нагрузки. Давайте рассмотрим простой пример:

import React, {
  Fragment,
  useState,
  useEffect,
  useReducer,
} from 'react';
import axios from 'axios';
 
const dataFetchReducer = (state, action) => {
  ...
};
 
const useDataApi = (initialUrl, initialData) => {
  const [url, setUrl] = useState(initialUrl);
 
  const [state, dispatch] = useReducer(dataFetchReducer, {
    isLoading: false,
    isError: false,
    data: initialData,
  });
 
  ...
};

useReducer принимает в качестве параметров функцию-редуктор и объект начального состояния. В нашем случае состояния данных, загрузки и ошибки имеют те же начальные значения, что и при создании useState, но они были объединены в один объект, созданный useReducer, а не в несколько состояний, созданных useState.

const dataFetchReducer = (state, action) => {
  ...
};
 
const useDataApi = (initialUrl, initialData) => {
  const [url, setUrl] = useState(initialUrl);
 
  const [state, dispatch] = useReducer(dataFetchReducer, {
    isLoading: false,
    isError: false,
    data: initialData,
  });
 
  useEffect(() => {
    const fetchData = async () => {
      dispatch({ type: 'FETCH_INIT' });
 
      try {
        const result = await axios(url);
 
        dispatch({ type: 'FETCH_SUCCESS', payload: result.data });
      } catch (error) {
        dispatch({ type: 'FETCH_FAILURE' });
      }
    };
 
    fetchData();
  }, [url]);
 
  ...
};

При выборке данных можно вызвать функцию отправки для отправки информации редюсеру. Параметр, отправляемый с помощью функции отправки, представляет собой объект с атрибутом типа и необязательным атрибутом полезной нагрузки. Атрибут type сообщает редьюсеру, какой переход состояния необходимо применить, и редьюсер может использовать полезную нагрузку для создания новых состояний. Здесь у нас есть только три перехода между состояниями: инициация запроса, успешное выполнение запроса и сбой запроса.

В конце пользовательских хуков состояние возвращается, как и раньше, но поскольку мы получаем объект состояния вместо отдельного состояния, которое мы использовали для деконструкции объекта состояния перед возвратом. Таким образом, люди, вызывающие пользовательские хуки useDataApi, по-прежнему могут получить доступ к трем состояниям данных: isLoading и isError.

const useDataApi = (initialUrl, initialData) => {
  const [url, setUrl] = useState(initialUrl);
 
  const [state, dispatch] = useReducer(dataFetchReducer, {
    isLoading: false,
    isError: false,
    data: initialData,
  });
 
  ...
 
  const doFetch = url => {
    setUrl(url);
  };
 
  return { ...state, doFetch };
}; 

Затем добавьте реализацию функции редуктора. Для этого требуются три различных перехода состояний FETCH_INIT, FETCH_SUCCESS и FETCH_FAILURE. Каждый переход состояния должен возвращать новый объект состояния. Давайте посмотрим, как это реализовать с помощью оператора switch case:

switch (action.type) {
    case 'FETCH_INIT':
      return {
        ...state,
        isLoading: true,
        isError: false
      };
    case 'FETCH_SUCCESS':
      return {
        ...state,
        isLoading: false,
        isError: false,
        data: action.payload,
      };
    case 'FETCH_FAILURE':
      return {
        ...state,
        isLoading: false,
        isError: true,
      };
    default:
      throw new Error();
  }
};

4.6 Отмена запросов данных

Очень распространенная проблема в React: если запрос отправлен в компоненте, и компонент выгружен до того, как запрос вернулся, он попытается установить это состояние в это время, и будет сообщено об ошибке. Нам нужно обработать эту ситуацию в хуках, вы можете увидеть, как это сделать:

const useDataApi = (initialUrl, initialData) => {
  const [url, setUrl] = useState(initialUrl);
 
  const [state, dispatch] = useReducer(dataFetchReducer, {
    isLoading: false,
    isError: false,
    data: initialData,
  });
 
  useEffect(() => {
    let didCancel = false;
    const fetchData = async () => {
      dispatch({ type: 'FETCH_INIT' });
      try {
        const result = await axios(url);
        if (!didCancel) {
          dispatch({ type: 'FETCH_SUCCESS', payload: result.data });
        }
      } catch (error) {
        if (!didCancel) {
          dispatch({ type: 'FETCH_FAILURE' });
        }
      }
    };
  fetchData();
    return () => {
      didCancel = true;
    };
  }, [url]);
 
  const doFetch = url => {
    setUrl(url);
  };
 
  return { ...state, doFetch };
};

Мы видим, что здесь добавлена ​​переменная didCancel, если эта переменная имеет значение true, то диспетчер не будет отправлен, а также не будет выполнено действие установки состояния. Здесь мы устанавливаем didCancel в true в функции возврата useEffe, и эта логика автоматически вызывается при выгрузке компонента. Это также позволяет избежать установки состояния незагруженного компонента.

5. useEffect и useLayoutEffect

1.png

  • useEffect не будет выполняться до тех пор, пока не будет завершен весь рендеринг.
  • useLayoutEffect будет выполняться после макета браузера и перед рисованием
  • Сигнатура его функции такая же, как и у useEffect, но эффект будет вызываться синхронно после всех изменений DOM.
  • Вы можете использовать его для чтения макета DOM и синхронного запуска повторного рендеринга.
  • Расписание обновления внутри useLayoutEffect будет обновляться синхронно до того, как браузер выполнит рисование.
  • По возможности используйте стандартный useEffect, чтобы избежать блокировки обновлений представления.
function LayoutEffect() {
   const [color, setColor] = useState('red');
   useLayoutEffect(() => {
       alert(color);
   });
   useEffect(() => {
       console.log('color', color);
   });
   return (
       <>
           <div id="myDiv" style={{ background: color }}>颜色</div>
           <button onClick={() => setColor('red')}>红</button>
           <button onClick={() => setColor('yellow')}>黄</button>
           <button onClick={() => setColor('blue')}>蓝</button>
       </>
   );
}

useEffectПреимущества
Используемый эффект выполнен в конце рендеринга, поэтому он не будет блокировать процесс рендеринга браузера, поэтому проекты, написанные с помощью функционального компонента, обычно имеют лучшую производительность.
Это, естественно, соответствует концепции React Fiber, потому что Fiber будет приостанавливать или обрезать очередь для выполнения рендеринга различных компонентов в зависимости от ситуации.Если код соответствует характеристикам Capture Value, безопасный доступ к значению будет гарантируется в среде Fiber, а ослабление жизненного цикла также может решить прерванное выполнение проблем, вызванных временем.
useEffect не будет выполняться при рендеринге на стороне сервера. Поскольку он выполняется после выполнения DOM, он гарантированно получит атрибуты DOM после того, как состояние вступит в силу.

6. Анализ исходного кода useEffect

Во-первых, мы должны помнить о некоторых свойствах хуков эффектов:

  • Они создаются во время рендеринга, но запускаются после того, как браузер их отрисует.
  • Если дана команда уничтожения, они будут уничтожены до следующего розыгрыша.
  • Они будут выполняться в установленном порядке.

Таким образом, должна быть еще одна очередь для хранения этих хуков эффектов, а также возможность их обнаружения после рисования. Как правило, волокно должно содержать очередь, содержащую узлы эффектов. Каждый узел эффекта относится к другому типу и может находиться в соответствующем состоянии:

Экземпляр getSnapshotBeforeUpdate() вызывается перед модификацией.

Запускает удаление всех вставок, обновлений, удалений и ссылок.

Запустите все функции жизненного цикла и функции обратного вызова ref. Функции жизненного цикла будут выполняться в отдельном канале, поэтому будут вызываться все замены, обновления и удаления во всем дереве компонентов. Этот процесс также запускает любые начальные хуки эффектов, специфичные для визуализатора.

Эффекты, отправляемые хуком useEffect(), также известные как «пассивные эффекты», основаны на этой части кода.

Эффекты ловушек будут храниться в свойстве волокна с именем updateQueue, каждый узел эффекта имеет следующую структуру:

  • тег — двоичное число, управляющее поведением узла эффекта
  • create - функция обратного вызова для запуска после рисования
  • destroy — это функция обратного вызова, возвращаемая функцией create(), и она будет запущена перед начальным рендерингом.
  • inputs — коллекция, значения которой будут определять, следует ли уничтожить узел эффекта или создать его заново
  • next - указывает на следующий узел эффекта, определенный в функциональном компоненте

За исключением атрибута tag, другие атрибуты очень лаконичны и просты для понимания. Если вы хорошо знакомы с хуками, вы должны знать, что React предоставляет некоторые хуки со специальными эффектами: например, useMutationEffect() и useLayoutEffect(). Оба этих хука эффекта используют useEffect() внутри, что на самом деле означает, что они создают хук эффекта, но с разными значениями свойств тега. Значение атрибута тега состоит из двоичных значений (подробности см. в исходном коде):

const NoEffect = /*             */ 0b00000000;
const UnmountSnapshot = /*      */ 0b00000010;
const UnmountMutation = /*      */ 0b00000100;
const MountMutation = /*        */ 0b00001000;
const UnmountLayout = /*        */ 0b00010000;
const MountLayout = /*          */ 0b00100000;
const MountPassive = /*         */ 0b01000000;
const UnmountPassive = /*       */ 0b10000000;

Типы хуков, поддерживаемые React Наиболее распространенным вариантом использования этих двоичных значений является использование конкатенации конвейера (|) для добавления битов к одному значению. Затем мы можем использовать амперсанд (&), чтобы проверить, вызывает ли атрибут тега определенное действие. Если результат отличен от нуля, значит да.

const effectTag = MountPassive | UnmountPassive
assert(effectTag, 0b11000000)
assert(effectTag & MountPassive, 0b10000000)

Пример использования бинарного шаблона проектирования React Вот эффекты ловушек, поддерживаемые React, и свойства их тегов (подробности см. в исходном коде):

  • Эффект по умолчанию —— UnmountPassive | MountPassive.
  • Эффект мутации —— UnmountSnapshot | MountMutation.
  • Эффект макета —— UnmountMutation | MountLayout.

А вот как React проверяет триггеры поведения (подробности см. в исходном коде):

if ((effect.tag & unmountTag) !== NoHookEffect) {
  // Unmount
}
if ((effect.tag & mountTag) !== NoHookEffect) {
  // Mount
}

Выдержка из исходного кода Итак, основываясь на том, что мы только что узнали о хуках эффектов, мы действительно можем вставить некоторые эффекты в волокно извне:

function injectEffect(fiber) {
  const lastEffect = fiber.updateQueue.lastEffect

  const destroyEffect = () => {
    console.log('on destroy')
  }

  const createEffect = () => {
    console.log('on create')

    return destroy
  }

  const injectedEffect = {
    tag: 0b11000000,
    next: lastEffect.next,
    create: createEffect,
    destroy: destroyEffect,
    inputs: [createEffect],
  }

  lastEffect.next = injectedEffect
}

const ParentComponent = (
  <ChildComponent ref={injectEffect} />
)

Это мой итог использованияЭффект.Автор очень хороший.Если есть ошибки прошу поправить меня.
Справочная статья:
nuggets.capable/post/684490… blog.CSDN.net/День мертвых_17775…