在React中如何使用useState钩子和回调函数?

225

我正在使用钩子函数功能组件。我需要从子组件更新父组件的状态。我在父组件中使用了一个prop函数。 除此之外,一切都正常,但我的prop函数获取的是先前的state而不是当前的state。我的prop函数在useState钩子设置当前状态之前被执行。 我该如何等待我的回调函数在useState调用后被执行?我正在寻找类组件中的setState(state, callback)之类的东西。

以下是代码片段:

function Parent() {
  const [Name, setName] = useState("");
  getChildChange = getChildChange.bind(this);
  function getChildChange(value) {
    setName(value);
  }

  return <div> {Name} :
    <Child getChildChange={getChildChange} ></Child>
  </div>
}

function Child(props) {
  const [Name, setName] = useState("");
  handleChange = handleChange.bind(this);

  function handleChange(ele) {
    setName(ele.target.value);
    props.getChildChange(collectState());
  }

  function collectState() {
    return Name;
  }

  return (<div>
    <input onChange={handleChange} value={Name}></input>
  </div>);
} 

7
为什么不直接传递 setName 方法并从子组件中调用它? - dan-klasson
希望我们在这个线程https://github.com/facebook/react/issues/17969中得到有趣的评论。 - RajaSekhar K
有一种简单的方法可以在不使用 useEffect 的情况下完成这个操作 https://dev59.com/C1QJ5IYBdhLWcg3wVEZ0#70405577 - Tunn
大多数答案都过于强调使其行为类似于基于类的组件。这是一个误导。真正的问题是子元素使用旧值调用了 getChildChange。将其更改为 props.getChildChange(ele.target.value) 将解决此问题。请参阅 https://dev59.com/blQI5IYBdhLWcg3w8wSR#56267744 - steinybot
8个回答

168

您可以使用 useEffect/useLayoutEffect 来实现这个:

const SomeComponent = () => {
  const [count, setCount] = React.useState(0)

  React.useEffect(() => {
    if (count > 1) {
      document.title = 'Threshold of over 1 reached.';
    } else {
      document.title = 'No threshold reached.';
    }
  }, [count]);

  return (
    <div>
      <p>{count}</p>

      <button type="button" onClick={() => setCount(count + 1)}>
        Increase
      </button>
    </div>
  );
};

如果您想防止回调在第一次呈现时运行,请调整先前的版本:

const SomeComponent = () => {
  const [count, setCount] = React.useState(0)

  const didMount = React.useRef(false);

  React.useEffect(() => {
    if (!didMount.current) {
      didMount.current = true;
      return;
    }

    if (count > 1) {
      document.title = 'Threshold of over 1 reached.';
    } else {
      document.title = 'No threshold reached.';
    }
  }, [count]);

  return (
    <div>
      <p>{count}</p>

      <button type="button" onClick={() => setCount(count + 1)}>
        Increase
      </button>
    </div>
  );
};

更多相关信息,请在此处查看。


我正在尝试安装use-state-with-callback,但它不起作用。它会显示一个错误。我该怎么办? - Efe FRK

150

setState(updater, callback)适用于useState

以下代码实现与类的原始setState回调非常接近。

已接受的答案进行了改进:

  1. 在初始渲染时省略了回调执行 - 我们只想在状态更新时调用它
  2. 每个setState调用的回调可以是动态的,就像使用类一样

用法

const App = () => {
  const [state, setState] = useStateCallback(0); // same API as useState

  const handleClick = () => {
    setState(
      prev => prev + 1,
      // second argument is callback, `s` being the *updated* state
      s => console.log("I am called after setState, state:", s)
    );
  };

  return <button onClick={handleClick}>Increment</button>;
}

useStateCallback

function useStateCallback(initialState) {
  const [state, setState] = useState(initialState);
  const cbRef = useRef(null); // init mutable ref container for callbacks

  const setStateCallback = useCallback((state, cb) => {
    cbRef.current = cb; // store current, passed callback in ref
    setState(state);
  }, []); // keep object reference stable, exactly like `useState`

  useEffect(() => {
    // cb.current is `null` on initial render, 
    // so we only invoke callback on state *updates*
    if (cbRef.current) {
      cbRef.current(state);
      cbRef.current = null; // reset callback after execution
    }
  }, [state]);

  return [state, setStateCallback];
}

function useStateCallback<T>(
  initialState: T
): [T, (state: T, cb?: (state: T) => void) => void] {
  const [state, setState] = useState(initialState);
  const cbRef = useRef<((state: T) => void) | undefined>(undefined); // init mutable ref container for callbacks

  const setStateCallback = useCallback((state: T, cb?: (state: T) => void) => {
    cbRef.current = cb; // store current, passed callback in ref
    setState(state);
  }, []); // keep object reference stable, exactly like `useState`

  useEffect(() => {
    // cb.current is `undefined` on initial render,
    // so we only invoke callback on state *updates*
    if (cbRef.current) {
      cbRef.current(state);
      cbRef.current = undefined; // reset callback after execution
    }
  }, [state]);

  return [state, setStateCallback];
}

更多信息:React Hooks FAQ:是否有类似实例变量的东西?

工作示例

const App = () => {
  const [state, setState] = useStateCallback(0);

  const handleClick = () =>
    setState(
      prev => prev + 1,
      // important: use `s`, not the stale/old closure value `state`
      s => console.log("I am called after setState, state:", s)
    );

  return (
    <div>
      <p>Hello Comp. State: {state} </p>
      <button onClick={handleClick}>Click me</button>
    </div>
  );
}

function useStateCallback(initialState) {
  const [state, setState] = useState(initialState);
  const cbRef = useRef(null);

  const setStateCallback = useCallback((state, cb) => {
    cbRef.current = cb; 
    setState(state);
  }, []);

  useEffect(() => {
    if (cbRef.current) {
      cbRef.current(state);
      cbRef.current = null;
    }
  }, [state]);

  return [state, setStateCallback];
}

ReactDOM.render(<App />, document.getElementById("root"));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.13.0/umd/react.production.min.js" integrity="sha256-32Gmw5rBDXyMjg/73FgpukoTZdMrxuYW7tj8adbN8z4=" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.13.0/umd/react-dom.production.min.js" integrity="sha256-bjQ42ac3EN0GqK40pC9gGi/YixvKyZ24qMP/9HiGW7w=" crossorigin="anonymous"></script>
<script>var { useReducer, useEffect, useState, useRef, useCallback } = React</script>
<div id="root"></div>


1
@bot19 这是回调函数的实际调用,该回调函数之前已通过“setState(..,cb)”进行设置。"cbRef.current"存储函数。然后使用当前更新的状态调用此函数 - (state)。 - ford04
备选方案:https://codesandbox.io/s/cocky-mendel-tqh58?file=/src/App.js - dwjohnston
1
@dwjohnston 在相同值的情况下“退出状态更新”是Hooks的新React默认值 - 因此在大多数情况下我不会改变这种行为。如果由于遗留原因需要与旧的基于类的比较保持一致(由于对象合并而表现出这种方式),那么codesandbox方法看起来是合理的!您可以每次将状态值包装在新的对象容器中,而不是使用“Symbol”。 - ford04
2
https://codesandbox.io/s/gifted-dhawan-hedsp?file=/src/App.tsx - 我使用了数组并在回调中添加了当前状态选项。还有一个双重更新的示例。希望能对某些人有所帮助 :) - Petr Újezdský
1
@PetrÚjezdský 感谢您的想法!关于第一点:我猜,这个评论很合适。关于第二点:如果在同一渲染周期和相同的钩子实例中调用setState两次,则在React中最后一个值将获胜。因此,当设置回调时,我期望具有相同的行为,并且会感到困惑,如果旧的新的回调都被调用。但是,这似乎是一个边缘情况 - 最可能的情况是您将拥有一个事件处理程序,在其中在不同的渲染中进行状态设置。 - ford04
显示剩余5条评论

43

使用React 16.x及以上版本,如果你想在使用 useState 钩子改变状态时调用回调函数,可以使用附加到状态改变的 useEffect 钩子。

import React, { useEffect } from "react";

useEffect(() => {
  props.getChildChange(name); // using camelCase for functions is recommended.
}, [name]); // this will call getChildChange on initial render and when ever name changes.

6
如果有多个功能,但只需要其中一个在回放时起作用,我们应该怎么办? - Gucal
1
@Gucal,你可以多次使用useEffect,例如:useEffect(() => loadFunctionAOnce())。useEffect(() => loadFunctionBIfNameChange(), [name]) - DAMIEN JIANG
2
这也将在初始渲染时运行props.getChildChange。 - 0xAnon

14

实际上,在使用React Hooks时应避免使用this,因为它会产生副作用。这就是React团队创建React Hooks的原因。

如果您删除尝试绑定this的代码,您只需简单地将ParentsetName传递给Child并在handleChange中调用它。代码更加清晰!

function Parent() {
  const [Name, setName] = useState("");

  return <div> {Name} :
    <Child setName={setName} ></Child>
  </div>
}

function Child(props) {
  const [Name, setName] = useState("");

  function handleChange(ele) {
    setName(ele.target.value);
    props.setName(ele.target.value);
  }

  return (<div>
    <input onChange={handleChange} value={Name}></input>
  </div>);
} 
此外,您不需要创建两份Name的副本(一个在Parent中,另一个在Child中)。 坚持“单一真相来源”原则,Child 不必拥有状态Name,而是从Parent接收它。 更干净的节点!
function Parent() {
  const [Name, setName] = useState("");

  return <div> {Name} :
    <Child setName={setName} Name={Name}></Child>
  </div>
}

function Child(props) {    
  function handleChange(ele) {
    props.setName(ele.target.value);
  }

  return (<div>
    <input onChange={handleChange} value={props.Name}></input>
  </div>);
} 

10

我们可以编写自定义函数,如果状态发生任何更改,将调用回调函数。

import React, { useState, useEffect } from "react";
import ReactDOM from "react-dom";

import "./styles.css";

const useStateCallbackWrapper = (initilValue, callBack) => {
  const [state, setState] = useState(initilValue);
  useEffect(() => callBack(state), [state]);
  return [state, setState];
};

const callBack = state => {
  console.log("---------------", state);
};
function App() {
  const [count, setCount] = useStateCallbackWrapper(0, callBack);
  return (
    <div className="App">
      <h1>{count}</h1>
      <button onClick={() => setCount(count + 1)}>+</button>
      <h2>Start editing to see some magic happen!</h2>
    </div>
  );
}

const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);

`


这个解决方案在生产构建中失败,出现 React Hook useEffect has a missing dependency: 'callBack'. Either include it or remove the dependency array. If 'callBack' changes too often, find the parent component that defines it and wrap that definition in useCallback react-hooks/exhaustive-deps 的错误提示。 - Pablo Albaladejo
尝试保持useEffect行如下: useEffect(() => callBack?callBack(state):null, [state, callBack]); - RajaSekhar K

6

另一种实现方法:

const [Name, setName] = useState({val:"", callback: null});
React.useEffect(()=>{
  console.log(Name)
  const {callback} = Name;
  callback && callback();
}, [Name]);
setName({val:'foo', callback: ()=>setName({val: 'then bar'})})


这很不错。这样,执行的顺序将取决于您设置键值的方式?首先是val,然后是callback? - Nick Chan Abdullah
我认为这是一个无用的解决方案。 - famfamfam

4

您可以使用useCallback钩子来完成这个操作。

function Parent() {
  const [name, setName] = useState("");
  const getChildChange = useCallback( (updatedName) => {
    setName(updatedName);
  }, []);

  return <div> {name} :
    <Child getChildChange={getChildChange} ></Child>
  </div>
}

function Child(props) {
  const [name, setName] = useState("");

  function handleChange(ele) {
    setName(ele.target.value);
    props.getChildChange(ele.target.value);
  }

  function collectState() {
    return name;
  }

  return (<div>
    <input onChange={handleChange} value={name}></input>
  </div>);
}

1
在两个组件中为同一变量设置状态对我来说似乎不是一个好主意。 - Isaac Pak
“useState Hook不支持第二个回调参数。要在渲染后执行副作用,请在组件主体中使用useEffect()声明它。” - Hidayt Rahman
嘿,@dishwasherWithProgrammingSkill,这段代码有什么用?与内联设置状态的方式 <Child getChildChange={(value) => setValue(value)} ></Child> 有何不同? - tmohammad78

3

function Parent() {
  const [Name, setName] = useState("");
  getChildChange = getChildChange.bind(this);
  function getChildChange(value) {
    setName(value);
  }

  return <div> {Name} :
    <Child getChildChange={getChildChange} ></Child>
  </div>
}

function Child(props) {
  const [Name, setName] = useState("");
  handleChange = handleChange.bind(this);
  collectState = collectState.bind(this);
  
  function handleChange(ele) {
    setName(ele.target.value);
  }

  function collectState() {
    return Name;
  }
  
   useEffect(() => {
    props.getChildChange(collectState());
   });

  return (<div>
    <input onChange={handleChange} value={Name}></input>
  </div>);
} 

useEffect作为componentDidMount和componentDidUpdate的替代品,因此在更新状态后它将起作用。


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