如何在React Hooks中使用`setState`回调函数

392

React hooks引入useState来设置组件状态。但是我该如何使用hooks来替换下面的回调函数:

setState(
  { name: "Michael" },
  () => console.log(this.state)
);

状态更新后我想做一些事情。

我知道我可以使用 useEffect 来完成额外的操作,但是我需要检查前一个状态值,这需要写一些代码。我正在寻找一个简单的解决方案,可以与 useState 钩子一起使用。


2
在类组件中,我使用了async和await来实现像你在setState中添加回调函数一样的结果。不幸的是,在hook中它并不起作用。即使我添加了async和await,React也不会等待状态更新。也许useEffect是唯一的解决方式。 - MING WU
有一种简单的方法可以在不使用 useEffect 的情况下完成这个。https://dev59.com/C1QJ5IYBdhLWcg3wVEZ0#70405577 - Tunn
23个回答

1

简单的解决方案,只需安装

npm i use-state-with-callback

import React from 'react';
import { useStateWithCallbackLazy } from "use-state-with-callback";

const initialFilters = {
  smart_filter: "",
};

const MyCallBackComp = () => {
  const [filters, setFilters] = useStateWithCallbackLazy(initialFilters);

  const filterSearchHandle = (e) => {
    setFilters(
      {
        ...filters,
        smart_filter: e,
      },
      (value) => console.log("smartFilters:>", value)
    );
  };

  return (
    <Input
      type="text"
      onChange={(e) => filterSearchHandle(e.target.value)}
      name="filter"
      placeholder="Search any thing..."
    />
  );
};

来源: React useState 回调函数


1

编辑

在这里使用Promise似乎仍然会推迟重新渲染后的执行,触发setState两次可能是获取最新状态的最佳解决方案。因为setState将被列出,我们只需要在重新渲染之前获取prevState来使用。

原始帖子

我刚刚想到,如果我们可以在这里使用一个Promise,让setState变得可等待。

这是我的实验结果,感觉比使用回调函数更好。

主要是在useEffect中暂时使用resolve函数来触发。

function useAsyncState(initialState) {
  const [state, setState] = useState(initialState)
  const resolveCb = useRef()

  const handleSetState = (updatedState) => new Promise((resolve, reject) => {
    // force previous promise resolved
    if (typeof resolveCb.current === 'function') {
      resolveCb.current(updatedState)
    }
    resolveCb.current = resolve
    try {
      setState(updatedState)
    } catch(err) {
      resolveCb.current = undefined
      reject(err)
    }
  })

  useEffect(() => {
    if (typeof resolveCb.current === 'function') {
      resolveCb.current(state)
      resolveCb.current = undefined
    }
  }, [state])

  return [state, handleSetState]
}

在组件中使用

function App() {
  const [count, setCount] = useAsyncState(0)

  const increment = useMemoizedFn(async () => {
    const newCount = await setCount(count + 1)
    console.log(newCount)
  })

  console.log('rerender')

  return (
    <div>
      <h3 onClick={increment}>Hi, {count}</h3>
    </div>
  )
}

1

我有一个非常具体的用例,需要在dom中渲染一个类,然后设置另一个类。这是我找到的相当优雅的解决方案。

const [value1, setValue1] = useState({value: 'whatever', onValue: false})


useEffect(() => {
    setValue1(prev => ({
      value: 'whatever',
      onValue: !prev.onValue, 
    }));
}, ['whatever'])

 
useEffect(() => {

// if you want to ensure the render happens before doThing2() then put it in a timeout of 1ms,
  setTimeout(doThing2, 1); 

// or if you are happy to call it immediately after setting value don't include the timeout
 doThing2()


}, [value1.onValue])

1
您的问题非常有价值。让我告诉您,useEffect默认情况下仅运行一次,并在每次依赖项数组更改后运行。
请查看下面的示例:
import React,{ useEffect, useState } from "react";

const App = () => {
  const [age, setAge] = useState(0);
  const [ageFlag, setAgeFlag] = useState(false);

  const updateAge = ()=>{
    setAgeFlag(false);
    setAge(age+1);
    setAgeFlag(true);
  };

  useEffect(() => {
    if(!ageFlag){
      console.log('effect called without change - by default');
    }
    else{
      console.log('effect called with change ');
    }
  }, [ageFlag,age]);

  return (
    <form>
      <h2>hooks demo effect.....</h2>
      {age}
      <button onClick={updateAge}>Text</button>
    </form>
  );
}

export default App;

如果您希望在使用hooks时执行setState回调,请使用标志变量并在useEffect内部给出IF ELSE或IF块,以便仅在满足条件时才执行该代码块。无论如何,effect会随着依赖项数组的更改而运行,但是effect内部的IF代码只会在特定条件下执行。

4
这样行不通。你不知道updateAge函数内的三个语句将以什么顺序运行,因为它们都是异步的。唯一可以保证的是第一行在第三行之前运行(因为它们都在同一个状态上操作),但你对第二行一无所知。这个例子太简单了,无法看清楚这一点。 - Mohit Singh
我的朋友Mohit。当我从React类转换到Hooks时,我在一个大型复杂的React项目中实现了这种技术,并且它完美地工作。只需在Hooks的任何位置尝试相同的逻辑来替换setState回调,你就会知道。 - arjun sah
4
“works in my project isn't an explanation”的意思是“在我的项目中运行正常并不能说明原因”。建议阅读文档。这些代码不是同步执行的。无法确定updateAge函数中三行代码的执行顺序。如果它们是同步执行的,那么为什么还需要flag呢?可以直接在setAge后面调用console.log()函数。 - Mohit Singh
1
useRef对于“ageFlag”是一个更好的解决方案。 - Dr.Flink

0

我认为使用 useRef 区分是否已挂载并不是一个好的方式,更好的方法是通过在 useEffect() 中确定 useState() 生成的值是否为初始值来实现。

const [val, setVal] = useState(null)

useEffect(() => {
  if (val === null) return
  console.log('not mounted, val updated', val)
}, [val])

0
我探索了使用state-with-callback npm库以及其他类似的自定义hooks,但最终我意识到我只需要像这样做:
const [user, setUser] = React.useState(
  {firstName: 'joe', lastName: 'schmo'}
)

const handleFirstNameChange=(val)=> {
  const updatedUser = {
     ...user,
     firstName: val
  }
  setUser(updatedUser)
  updateDatabase(updatedUser)
}

2
在这种情况下,这肯定有效,但如果您想在更改状态后立即触发某些操作,但仅在实际异步更改后才需要 useEffect 或其中一个使用 useEffect 的自定义钩子。 希望提到这一点,因为这可能会让其他人感到困惑。 - Can Rau

0
自定义钩子用于带有回调的useState
import { useCallback, useEffect, useRef, useState } from 'react';

// Define a generic function type for the updater and the callback
type Updater<T> = T | ((prevState: T) => T);
type Callback<T> = (state: T) => void;

function useStateCallback<T>(initialState: T): [T, (stateUpdater: Updater<T>, cb?: Callback<T>) => void] {
    const [state, setState] = useState<T>(initialState);
    const cbRef = useRef<Callback<T> | undefined>(undefined); // Ref to hold the callback

    const setStateCallback = useCallback(
        (stateUpdater: Updater<T>, cb?: Callback<T>) => {
            cbRef.current = cb; // Store the callback in ref
            // Set the state, handle function type updater for prevState
            setState(prevState => typeof stateUpdater === 'function' 
                ? (stateUpdater as (prevState: T) => T)(prevState) 
                : stateUpdater);
        }, 
        []
    );

    // useEffect to call the callback after state update
    useEffect(() => {
        if (cbRef.current) {
            cbRef.current(state); // Call the callback with the updated state
            cbRef.current = undefined; // Reset the callback ref
        }
    }, [state]);

    return [state, setStateCallback];
}

export default useStateCallback;

这个钩子增加了一些复杂性,但在使用上有效地模拟了类组件中的setState钩子。
潜在的用法:
import React from 'react';
import useStateCallback from './useStateCallback';

const ExampleComponent: React.FC = () => {
    const [count, setCount] = useStateCallback(0);

    // Example usage of setState with a callback
    const incrementAndLog = () => {
        setCount(count + 1, (newCount) => {
            console.log(`Count updated to: ${newCount}`);
        });
    };

    return (
        <div>
            <p>Count: {count}</p>
            <button onClick={incrementAndLog}>Increment</button>
        </div>
    );
};

export default ExampleComponent;

0

如果您不需要异步更新状态,可以使用 ref 来保存值,而不是使用 useState

const name = useRef("John");
name.current = "Michael";
console.log(name.current); // will print "Michael" since updating the ref is not async

0

在我们拥有原生内置的setState回调支持之前,我们可以采用纯JavaScript的方式...直接调用函数并将新变量直接传递给它。

  const [counter, setCounter] = useState(0);

  const doSomething = () => {
    const newCounter = 123
    setCounter(newCounter);
    doSomethingWCounter(newCounter);
  };

  function doSomethingWCounter(newCounter) {
    console.log(newCounter); // 123
  }

5
当setState完成时,不能保证会调用doSomethingWCounter。 - Design by Adrian

-1

传递一个函数怎么样?

const [name, setName] = useState(initialName); 
...
setName(() => {
    const nextName = "Michael";
    console.log(nextName);
    return nextName;
  });

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接