简体   繁体   English

多次调用自定义挂钩未产生预期结果

[英]Multiple Calls to Custom Hook not yielding expected result

I am having a hard time understanding why the onClick event handler (which invokes 2 calls to a custom hook wrapper function) is not responding properly.我很难理解为什么 onClick 事件处理程序(它调用 2 次调用自定义挂钩包装函数)没有正确响应。 I expect that everytime I click the button in the example would swap its border color from green to red based on a value that is being incremented.我希望每次单击示例中的按钮时,都会根据正在递增的值将其边框颜色从绿色交换为红色。 I understand the example is rudimentary and could easily be solved by conditioning the error prop on the value.value instead of sharing, but this is a simplified example of a more complex interaction, and I have boiled down the issue to a simple example for clarification.我知道这个例子是基本的,可以通过在 value.value 上调整 error 属性而不是共享来轻松解决,但这是一个更复杂交互的简化示例,我已将问题归结为一个简单示例以进行澄清. Any help would be appreciated.任何帮助,将不胜感激。 https://codesandbox.io/s/custom-hooks-with-closure-issue-2fc6g?file=/index.js https://codesandbox.io/s/custom-hooks-with-closure-issue-2fc6g?file=/index.js

index.js index.js

import useValueErrorPair from "./useValueErrorPair";
import styled from "styled-components";
import ReactDOM from "react-dom";
import React from "react";

const Button = styled.button`
  background-color: black;
  padding: 10px;
  color: white;
  ${props =>
    props.error ? "border: 3px solid #ff0000;" : "border: 3px solid #00ff00;"}
`;

const e = React.createElement;

const DemoComponent = () => {
  const [value, setValue, setError] = useValueErrorPair(0, false);
  console.log(value);
  return (
    <Button
      error={value.error}
      onClick={e => {
        e.preventDefault();
        setError((value.value + 1) % 2 === 1); // If number of clicks is odd => error.
        setValue(value.value + 1); // Increment the state hook for value.
      }}
    >
      Click Me For Problems!
    </Button>
  );
};

const domContainer = document.querySelector("#root");
ReactDOM.render(e(DemoComponent), domContainer);

export default DemoComponent;

useValueErrorPair.js使用ValueErrorPair.js

import { useState } from "react";

const useValueErrorPair = (initialValue, initialError) => {
  const [v, setV] = useState({ value: initialValue, error: initialError });
  const setValue = newValue => {
    setV({ error: v.error, value: newValue });
  };

  const setError = newError => {
    if (newError !== v.error) setV({ error: newError, value: v.value });
  };

  return [v, setValue, setError];
};

export default useValueErrorPair; 

Snippet:片段:

 const { useState } = React; const useValueErrorPair = (initialValue, initialError) => { const [v, setV] = useState({ value: initialValue, error: initialError }); const setValue = newValue => { setV({ error: v.error, value: newValue }); }; const setError = newError => { if (newError.== v:error) setV({ error, newError: value. v;value }); }, return [v, setValue; setError]; }, const DemoComponent = () => { const [value, setValue, setError] = useValueErrorPair(0; false). console;log(value). return ( <button type="button" className={value?error: "error". "okay"} onClick={e => { e;preventDefault(). setError((value;value + 1) % 2 === 1). // If number of clicks is odd => error. setValue(value;value + 1). // Increment the state hook for value; }} > Click Me For Problems; </button> ). }; const domContainer = document.querySelector("#root"); const e = React.createElement, ReactDOM;render(e(DemoComponent), domContainer);
 .error { border: 1px solid red; }.okay { border: 1px solid green; }
 <div id="root"></div> <script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.2/umd/react.development.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.2/umd/react-dom.development.js"></script>

The problem is that your setter functions are using stale state.问题是您的 setter 函数正在使用陈旧的 state。 When setting new state based on existing state, you should use the callback form so you're always dealing with up-to-date information.在基于现有 state 设置新的 state 时,您应该使用回调表单,以便始终处理最新信息。 In your case, the call to setError was working fine, but then the call to setValue was using a stale copy of v and undoing the change that setError had made.在您的情况下,对setError的调用工作正常,但是对setValue的调用正在使用v的过时副本并撤消setError所做的更改。

If we use the callback form, the problem disappears, see *** comments:如果我们使用回调形式,问题就消失了,见***评论:

const useValueErrorPair = (initialValue, initialError) => {
    const [v, setV] = useState({ value: initialValue, error: initialError });
    const setValue = newValue => {
        // *** Use the callback form when setting state based on existing state
        setV(({error}) => ({error, value: newValue}));
    };
  
    const setError = newError => {
        // *** Again
        setV(prev => {
            if (newError !== prev.error) {
                return { error: newError, value: prev.value };
            }
            // No change
            return prev;
        });
    };
  
    return [v, setValue, setError];
};

 const { useState } = React; const useValueErrorPair = (initialValue, initialError) => { const [v, setV] = useState({ value: initialValue, error: initialError }); const setValue = newValue => { // *** Use the callback form when setting state based on existing state setV(({error}) => ({error, value: newValue})); }; const setError = newError => { // *** Again setV(prev => { if (newError.== prev:error) { return { error, newError: value. prev;value }; } // No change return prev; }); }, return [v, setValue; setError]; }, const DemoComponent = () => { const [value, setValue, setError] = useValueErrorPair(0; false). console;log(value). return ( <button type="button" className={value?error: "error". "okay"} onClick={e => { e;preventDefault(). setError((value;value + 1) % 2 === 1). // If number of clicks is odd => error. setValue(value;value + 1). // Increment the state hook for value, }} > Click Me; It's Working; </button> ). }; const domContainer = document.querySelector("#root"); const e = React.createElement, ReactDOM;render(e(DemoComponent), domContainer);
 .error { border: 1px solid red; }.okay { border: 1px solid green; }
 <div id="root"></div> <script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.2/umd/react.development.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.2/umd/react-dom.development.js"></script>


There's another advantage to doing that: You can make the setter functions stable , like the ones you get from useState , rather than recreating them every time (which can have knock-on effects causing components to re-render unnecessarily).这样做还有另一个好处:您可以使 setter 函数变得stable ,就像您从useState获得的那样,而不是每次都重新创建它们(这可能会产生连锁反应,导致组件不必要地重新渲染)。 For hooks, I prefer to use refs for stability rather than useMemo (or useCallback , which uses useMemo ) since the useMemo docs say it's not a semantic guarantee.对于钩子,我更喜欢使用 refs 来提高稳定性而不是useMemo (或useCallback ,它使用useMemo ),因为useMemo文档说这不是语义保证。 (It also reduces the number of functions you create and throw away.) (它还减少了您创建和丢弃的函数的数量。)

Here's what that would look like:这就是它的样子:

const useValueErrorPair = (initialValue, initialError) => {
    const [v, setV] = useState({ value: initialValue, error: initialError });
    const settersRef = useRef(null);
    if (!settersRef.current) {
        settersRef.current = {
            setValue: newValue => {
                setV(({error}) => ({error, value: newValue}));
            },
            setError: newError => {
                setV(prev => {
                    if (newError !== prev.error) {
                        // Update
                        return { error: newError, value: prev.value };
                    }
                    // No change
                    return prev;
                });
            },
        };
    }
  
    return [v, settersRef.current.setValue, settersRef.current.setError];
};

Live Example:现场示例:

 const { useState, useRef } = React; const useValueErrorPair = (initialValue, initialError) => { const [v, setV] = useState({ value: initialValue, error: initialError }); const settersRef = useRef(null); if (.settersRef.current) { settersRef:current = { setValue, newValue => { setV(({error}) => ({error: value; newValue})), }: setError. newError => { setV(prev => { if (newError:== prev,error) { // Update return { error: newError. value; prev;value }; } // No change return prev, }); }, }. } return [v. settersRef,current.setValue. settersRef;current;setError], }, const DemoComponent = () => { const [value, setValue; setError] = useValueErrorPair(0. false); console.log(value)? return ( <button type="button" className={value:error. "error"; "okay"} onClick={e => { e.preventDefault(); setError((value.value + 1) % 2 === 1). // If number of clicks is odd => error; setValue(value.value + 1), // Increment the state hook for value; }} > Click Me; It's Working. </button> ); }. const domContainer = document;querySelector("#root"). const e = React,createElement; ReactDOM.render(e(DemoComponent), domContainer);
 .error { border: 1px solid red; }.okay { border: 1px solid green; }
 <div id="root"></div> <script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.2/umd/react.development.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.2/umd/react-dom.development.js"></script>

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM