Несколько вызовов пользовательского перехвата не дают ожидаемого результата

#javascript #reactjs #react-hooks

#javascript #reactjs #реагирующие перехваты

Вопрос:

Мне трудно понять, почему обработчик события onClick (который вызывает 2 вызова функции-оболочки пользовательского хука) не отвечает должным образом. Я ожидаю, что каждый раз, когда я нажимаю кнопку в примере, цвет границы будет меняться с зеленого на красный в зависимости от значения, которое увеличивается. Я понимаю, что пример является элементарным и может быть легко решен путем привязки значения ошибки к значению.значение вместо совместного использования, но это упрощенный пример более сложного взаимодействия, и я свел проблему к простому примеру для пояснения. Любая помощь будет оценена. https://codesandbox.io/s/custom-hooks-with-closure-issue-2fc6g?file=/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

 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; 
 

Фрагмент:

 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> 

Комментарии:

1. Не используйте два «файла», создайте единый комбинированный фрагмент со всеми ненужными (например, styled-components ) удаленными, чтобы мы могли сосредоточиться на реальной проблеме (хук).

2. Я сделал это для вас, используя код из вопроса.

3. Извините, мой ответ был неверным вкратце (я пропустил часть необходимых изменений setError ). Теперь это исправлено.

4. И спасибо за редактирование кода в вопросе!

5. Спасибо, что исправили это. Я отвлекся на собрание и отказался от этого на несколько часов.

Ответ №1:

Проблема в том, что ваши функции установки используют устаревшее состояние. При настройке нового состояния на основе существующего состояния вам следует использовать форму обратного вызова, чтобы вы всегда имели дело с актуальной информацией. В вашем случае вызов setError работал нормально, но затем вызов setValue использовал устаревшую копию v и отменял setError внесенные изменения.

Если мы используем форму обратного вызова, проблема исчезает, см. *** Комментарии:

 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> 


В этом есть еще одно преимущество: Вы можете сделать установочные функции стабильными, как те, которые вы получаете useState , вместо того, чтобы воссоздавать их каждый раз (что может иметь побочные эффекты, приводящие к повторному рендерингу компонентов без необходимости). Для хуков я предпочитаю использовать ссылки для стабильности, а не useMemo (или useCallback , который использует useMemo ), поскольку в useMemo документах говорится, что это не семантическая гарантия. (Это также уменьшает количество функций, которые вы создаете и выбрасываете.)

Вот как это будет выглядеть:

 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 { 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> 

Комментарии:

1. Это отличный материал. Под стабильным вы подразумеваете, что будет только одна ссылка на функции setValue и setError, а не новые экземпляры этих функций, создаваемые каждый раз, когда другой компонент вызывает пользовательский хук useValueErrorPair? Я думаю, это также работает, потому что вы используете функциональную форму setState части хука useState (которую, я признаю, я раньше не видел).

2. @CodeoftheWarrior — Да, точно, как установщик из useState : «Примечание: React гарантирует, что идентификатор функции setState стабилен и не изменится при повторном рендеринге». Нам все равно нужно использовать форму обратного вызова (из-за проблемы устаревшего состояния), и это также позволяет это сделать. Счастливого кодирования!