useEffect ReactJS hook 的 6 个使用案例
当我们需要在应用程序中使用副作用时,使用这种useEffect方式是最佳选择。除了 JavaScript 处理非原始数据类型的方式之外,这种钩子不会带来太多复杂情况。
根据官方文档,特效会在每次渲染完成后运行,但您可以选择仅在特定值发生变化时触发它们。此钩子使用一个“依赖项”数组:这些依赖项是useEffect监听变化的变量或状态。当它们的值发生变化时,钩子的主要部分useEffect就会执行。
此钩子用于清理正在运行的方法,例如定时器。首次调用此钩子时,会首先执行其主体部分。之后每次调用此钩子时,都会先执行 return语句return,然后再执行钩子主体部分。这种行为对于在再次运行代码之前清理已在运行的代码尤为有用,从而可以防止内存泄漏。
当使用非原始 JavaScript 数据类型(例如数组、对象、函数)作为依赖项时,此钩子会出现一种有趣的行为。对于原始值(例如数字和字符串),我们可以从另一个变量定义一个变量,而它们的值将相同:
const a = 1
const b = 1
a === b
// Output: true
但对于非原始值(例如对象),这种行为并不相同:
{} === {}
// Output: false
因此,在使用对象作为依赖项时,我们需要格外小心,因为即使它们看起来像是未经修改的数据,实际上也可能并非如此。我们或许应该使用对象的属性作为依赖项,而不是直接使用对象本身:
useEffect(() => {
// Some code that uses the properties
}, [myObject.property1, myObject.property2]);
现在,让我们来看一些这个钩子的使用案例。
useEffect用例
- 挂载时运行一次:获取 API 数据
- 状态改变时运行:验证输入字段
- 基于状态变化运行:实时过滤
- 状态改变时运行:数组值变化时触发动画
- 基于props 更改运行:根据获取的 API 数据更新段落列表
- 基于属性更改运行:更新获取的 API 数据以获取最新的 BTC 价格
挂载时运行一次:获取 API 数据
当我们只想执行一次操作时,尤其是在应用加载或挂载时,我们可以使用这种方法。在本例中,我们在应用挂载时useEffect触发一个GET 请求,并使用一个空数组作为依赖项。fetch()useEffect
import { useState, useEffect } from "react";
const UseCaseFetchApi = props => {
// useState is needed in order to display the result on the screen
const [bio, setBio] = useState({});
// 'async' shouldn't be used in the useEffect callback function because these callbacks are synchronous to prevent race conditions. We need to put the async function inside.
useEffect(() => {
const fetchData = async () => {
const response = await fetch('https://swapi.dev/api/people/1/');
const data = await response.json();
console.log(data);
setBio(data);
};
fetchData();
}, []);
// Empty dependencies array will make useEffect to run only once at startup because that array never changes
return (
<>
<hr />
<h2>useEffect use case</h2>
<h3>Running once on mount: fetch API data</h3>
<p>Luke Skywalker's bio:</p>
<pre>{JSON.stringify(bio, null, '\t')}</pre>
</>
);
};
export default UseCaseFetchApi;
状态改变时运行:验证输入字段
在接收字符的同时验证输入是另一个很棒的应用场景useEffect。虽然输入内容会通过某种方式存储在某个状态中useState,但每次输入内容发生变化时都会进行验证,从而为用户提供即时反馈。
我们可以添加一个setTimeout()函数,在一段时间后检查输入字段,从而延迟每次用户按键时的检查。我们需要clearTimeout()在钩子的返回语句中使用函数来清除这个计时器useEffect。类似的示例已在useEffect后面的动画触发器中实现。
import { useEffect, useState } from "react";
const UseCaseInputValidation = props => {
const [input, setInput] = useState('');
const [isValid, setIsValid] = useState(false);
const inputHandler = e => {
setInput(e.target.value);
};
useEffect(() => {
if (input.length < 5 || /\d/.test(input)) {
setIsValid(false);
} else {
setIsValid(true);
}
}, [input]);
return (
<>
<hr />
<h2>useEffect use case</h2>
<h3>Running on state change: validating input field</h3>
<form>
<label htmlFor="input">Write something (more than 5 non numerical characters is a valid input)</label><br />
<input type="text" id="input" autoComplete="off" onChange={inputHandler} style={{ height: '1.5rem', width: '20rem', marginTop: '1rem' }} />
</form>
<p><span style={isValid ? { backgroundColor: 'lightgreen', padding: '.5rem' } : { backgroundColor: 'lightpink', padding: '.5rem' }}>{isValid ? 'Valid input' : 'Input not valid'}</span></p>
</>
);
};
export default UseCaseInputValidation;
基于状态变化运行:实时过滤
我们可以通过useEffect在输入框中输入字母来“动态”过滤数组。为此,我们需要使用一个状态来保存输入内容,并在数组内部实现一个过滤器,useEffect该过滤器会在输入内容改变时触发(这得益于useEffect依赖关系)。
import { useEffect, useState } from "react";
const array = [
{ key: '1', type: 'planet', value: 'Tatooine' },
{ key: '2', type: 'planet', value: 'Alderaan' },
{ key: '3', type: 'starship', value: 'Death Star' },
{ key: '4', type: 'starship', value: 'CR90 corvette' },
{ key: '5', type: 'starship', value: 'Star Destroyer' },
{ key: '6', type: 'person', value: 'Luke Skywalker' },
{ key: '7', type: 'person', value: 'Darth Vader' },
{ key: '8', type: 'person', value: 'Leia Organa' },
];
const UseCaseLiveFilter = props => {
const [inputValue, setInputValue] = useState('');
const [inputType, setInputType] = useState('');
const [filteredArray, setFilteredArray] = useState(array);
const inputValueHandler = e => {
setInputValue(e.target.value);
};
const inputTypeHandler = e => {
setInputType(e.target.value);
};
useEffect(() => {
setFilteredArray((_) => {
const newArray = array.filter(item => item.value.includes(inputValue)).filter(item => item.type.includes(inputType));
return newArray;
});
}, [inputValue, inputType]);
// Prepare array to be rendered
const listItems = filteredArray.map((item) =>
<>
<tr>
<td style={{ border: '1px solid lightgray', padding: '0 1rem' }}>{item.type}</td>
<td style={{ border: '1px solid lightgray', padding: '0 1rem' }} > {item.value}</td>
</tr >
</>
);
return (
<>
<hr />
<h2>useEffect use case</h2>
<h3>Running on state change: live filtering</h3>
<form style={{ maxWidth: '23rem', display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<div>
<label htmlFor="input-type">Filter by <b>type</b></label><br />
<input type="text" id="input-type" autoComplete="off" onChange={inputTypeHandler} style={{ height: '1.5rem', width: '10rem', marginTop: '1rem' }} />
</div>
<div>
<label htmlFor="input-value">Filter by <b>value</b></label><br />
<input type="text" id="input-value" autoComplete="off" onChange={inputValueHandler} style={{ height: '1.5rem', width: '10rem', marginTop: '1rem' }} />
</div>
</form>
<br />
<table style={{ width: '20rem', border: '1px solid gray', padding: '0 1rem' }}>
<tr>
<th>Type</th>
<th>Value</th>
</tr>
{listItems}
</table>
</>
);
};
export default UseCaseLiveFilter;
状态改变时运行:数组值变化时触发动画
我们可以使用useEffect钩子函数,在向购物车添加新产品时触发动画效果。在这种情况下,我们需要一个状态来管理购物车中的商品,以及另一个状态来处理动画触发。
由于我们在内部使用了计时器useEffect,因此最好在再次设置计时器之前,使用return语句清除计时器useEffect,该语句会在钩子函数的主体部分useEffect被评估之前执行(第一次渲染除外)。
import { useState, useEffect } from 'react';
import classes from './UseCaseAnimation.module.css';
const products = [
'Death Star',
'CR90 corvette',
'Millennium Falcon',
'X-wing fighter',
'TIE fighter'
];
const UseCaseAnimation = props => {
const [cart, setCart] = useState([]);
const [triggerAnimation, setTriggerAnimation] = useState(false);
// Add item to the cart (array)
const clickHandler = e => {
e.preventDefault();
setCart(prevCart => {
const newCart = [...prevCart];
newCart.push(e.target.value);
return newCart;
});
};
// Clear the cart (array)
const clearHandler = e => {
e.preventDefault();
setCart([]);
};
// Trigger cart animation
useEffect(() => {
setTriggerAnimation(true);
const timer = setTimeout(() => {
setTriggerAnimation(false);
}, 900); // The duration of the animation defined in the CSS file
// Clear the timer before setting a new one
return () => {
clearTimeout(timer);
};
}, [cart]);
const cartClasses = triggerAnimation ? `${classes['jello-horizontal']} ${classes.cart}` : classes.cart;
const itemsOnSale = products.map(itemOnSale => {
return <li><form><span className={classes.item}>{itemOnSale} <button onClick={clickHandler} value={`"${itemOnSale}"`}>Add to cart</button></span></form></li >;
});
const cartItems = cart.map(item => {
return <li>{item}</li>;
});
return (
<>
<hr />
<h2>useEffect use case</h2>
<h3>Running on state change: trigger animation on new array value</h3>
<h4 style={{ color: 'blue' }}>Starship Marketplace</h4>
<ul>
{itemsOnSale}
</ul>
<div className={cartClasses}><span>Cart</span></div>
<div>
<p>Elements in cart:</p>
<ul>
{cartItems}
</ul>
</div>
<form><button className={classes.margin} onClick={clearHandler} value="clear">Clear cart</button></form>
</>
);
};
export default UseCaseAnimation;
基于props 更改运行:根据获取的 API 数据更新段落列表
在这个用例中,我们希望根据更新的调用触发状态更新fetch()。我们将获取到的数据发送到子组件,每当该数据发生变化时,子组件都会重新处理它。
import { useState, useEffect, useCallback } from "react";
const BaconParagraphs = props => {
const [baconParagraphText, setBaconParagraphText] = useState([]);
useEffect(() => {
setBaconParagraphText(props.chopBacon.map(piece => <p key={Math.random()}>{piece}</p>));
}, [props.chopBacon]); // Props
return (
<>
<p>Number of paragraphs: {baconParagraphText.length}</p>
{baconParagraphText}
</>
);
};
const UseCaseUpdateFetch = () => {
const [bacon, setBacon] = useState([]);
const [isLoading, setIsLoading] = useState(false);
const submitHandler = async e => {
e.preventDefault();
setIsLoading(true);
const response = await fetch(`https://baconipsum.com/api/?type=all-meat¶s=${e.target.paragraphs.value}&start-with-lorem=1`);
const data = await response.json();
setIsLoading(false);
setBacon(data);
};
return (
<>
<hr />
<h2>useEffect use case</h2>
<h3>Running on props change: update paragraph list on fetched API data update</h3>
<form onSubmit={submitHandler}>
<label htmlFor="paragraphs" style={{ display: "block", marginBottom: "1rem" }}>How many paragraphs of "Bacon ipsum" do you want?</label>
<select id="paragraphs" name="paragraphs">
<option value="1">1</option>
<option value="2">2</option>
<option value="3">3</option>
<option value="4">4</option>
</select>
<input type="submit" value="Show me the bacon!" style={{ marginLeft: "1rem" }} /> {isLoading && <span>Getting paragraphs... 🐷</span>}
</form>
<BaconParagraphs chopBacon={bacon} />
</>
);
};
export default UseCaseUpdateFetch;
基于props 更改运行:更新获取的 API 数据以获取最新的 BTC 价格
在这个例子中,useEffect我们使用 API 每 3 秒获取一次新数据。子组件useEffect接收时间作为依赖项,并且每次该依赖项发生变化时,fetch()都会触发一个新的事件。这样,我们就可以在应用程序中实时更新 BTC 汇率。
import { useState, useEffect } from "react";
import classes from './UseCaseUpdateApi.module.css';
// SECTION - Functions
const getCurrentTime = () => {
const now = new Date();
const time = now.getHours() + ':' + ('0' + now.getMinutes()).slice(-2) + ':' + ('0' + now.getSeconds()).slice(-2);
return time;
};
// SECTION - Components
const ExchangeRate = props => {
const [exchangeRate, setExchangeRate] = useState(0);
const [isAnimated, setIsAnimated] = useState(false);
useEffect(() => {
const getExchangeRate = async () => {
// Please don't abuse my personal API key :)
const response = await fetch("https://api.nomics.com/v1/exchange-rates?key=86983dc29fd051ced016bca55e301e620fcc51c4");
const data = await response.json();
console.log(data.find(item => item.currency === "BTC").rate);
setExchangeRate(data.find(item => item.currency === "BTC").rate);
};
getExchangeRate();
// Triggering animation
setIsAnimated(true);
const classTimer = setTimeout(() => {
setIsAnimated(false);
}, 1500);
// Clear the timer before setting a new one
return () => {
clearTimeout(classTimer);
setExchangeRate(exchangeRate); // Preventing Warning: Can't perform a React state update on an unmounted component. This is a no-op, but it indicates a memory leak in your application. To fix, cancel all subscriptions and asynchronous tasks in a useEffect cleanup function.
};
}, [props.onTime]);
const priceClasses = isAnimated ? `${classes.price} ${classes.heartbeat}` : `${classes.price}`;
return <div className={priceClasses}>USD <b>{exchangeRate}</b></div>;
};
const UseCaseUpdateApi = props => {
const [time, setTime] = useState(getCurrentTime());
// Trigger the update interval on startup (mount)
useEffect(() => {
const interval = setInterval(() => {
setTime(getCurrentTime());
}, 3000);
return () => clearInterval(interval);
}, []); // Empty dependencies array, so it will run once at mount and keep running 'in the background'
console.log(time);
return (
<>
<hr />
<h2>useEffect use case</h2>
<h3>Running on props change: updating fetched API data to get updated BTC price</h3>
<span>Last updated: {time} (polling every 3 seconds)</span><ExchangeRate onTime={time} />
</>
);
};
export default UseCaseUpdateApi;
最后,您可以在这里查看这些用例的实时演示,也可以在这里找到源代码。
文章来源:https://dev.to/colocodes/6-use-cases-of-the-useeffect-reactjs-hook-282o