发布于 2026-01-05 3 阅读
0

useEffect ReactJS hook 的 6 个使用案例

useEffect ReactJS hook 的 6 个使用案例

当我们需要在应用程序中使用副作用时,使用这种useEffect方式是最佳选择。除了 JavaScript 处理非原始数据类型的方式之外,这种钩子不会带来太多复杂情况。

根据官方文档,特效会在每次渲染完成后运行,但您可以选择仅在特定值发生变化时触发它们。此钩子使用一个“依赖项”数组:这些依赖项是useEffect监听变化的变量或状态。当它们的值发生变化时,钩子的主要部分useEffect就会执行。

此钩子用于清理正在运行的方法,例如定时器。首次调用此钩子时,会首先执行其主体部分。之后每次调用此钩子时,都会先执行 return语句return,然后再执行钩子主体部分。这种行为对于在再次运行代码之前清理已在运行的代码尤为有用,从而可以防止内存泄漏。

当使用非原始 JavaScript 数据类型(例如数组、对象、函数)作为依赖项时,此钩子会出现一种有趣的行为。对于原始值(例如数字和字符串),我们可以从另一个变量定义一个变量,而它们的值将相同:

const a = 1
const b = 1
a === b
// Output: true
Enter fullscreen mode Exit fullscreen mode

但对于非原始值(例如对象),这种行为并不相同:

{} === {}
// Output: false
Enter fullscreen mode Exit fullscreen mode

因此,在使用对象作为依赖项时,我们需要格外小心,因为即使它们看起来像是未经修改的数据,实际上也可能并非如此。我们或许应该使用对象的属性作为依赖项,而不是直接使用对象本身:

useEffect(() => {
        // Some code that uses the properties
    }, [myObject.property1, myObject.property2]);
Enter fullscreen mode Exit fullscreen mode

现在,让我们来看一些这个钩子的使用案例。

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;
Enter fullscreen mode Exit fullscreen mode

状态改变时运行:验证输入字段

在接收字符的同时验证输入是另一个很棒的应用场景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;
Enter fullscreen mode Exit fullscreen mode

基于状态变化运行:实时过滤

我们可以通过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;
Enter fullscreen mode Exit fullscreen mode

状态改变时运行:数组值变化时触发动画

我们可以使用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;
Enter fullscreen mode Exit fullscreen mode

基于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&paras=${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;
Enter fullscreen mode Exit fullscreen mode

基于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;
Enter fullscreen mode Exit fullscreen mode

最后,您可以在这里查看这些用例的实时演示,也可以在这里找到源代码

文章来源:https://dev.to/colocodes/6-use-cases-of-the-useeffect-reactjs-hook-282o