React: Как создать в функциональных компонентах функцию, зависящую от состояния?

В моем приложении есть такой компонент:

const MyComponent = props => {

    const { attrOneDefault, attrTwoDefault, formControl } = props;  
    const [inputValue, setInputValue] = useState({
        attr_one: attrOneDefault,
        attr_two: attrTwoDefault
    });

    const getValue = ( attr ) => {
        return inputValue[attr];
    }
    const setValue = ( attr, val ) => {
        if( attr === 'attr_one' ) {
            if( val === 'bar' && getValue(attr) !== 'foo' ) {
                val = 'foo bar';
            }
        }
        setInputValue( {...inputValue, [attr]: val} );
    }

    useEffect( () => {
        if( formControl ) {         
            Object.keys(inputValue).forEach( attribute => {
                formControl.subscribeToValueCollecting( attribute, () => {
                    return getValue(attribute);
                });
                formControl.subscribeToValueChange( attribute, ( value ) => {
                    setValue( attribute, value );
                    return true;
                });
            });
        }

        return () => { 
            if( formControl ) {
                Object.keys(inputValue).forEach( attribute => formControl.unsubscribe(attribute) );
            }
        }
    }, []);

    return (
        <div class="form-field">
            <input
                type="text"
                value={getValue('attr_one')}
                onChange={ e => setValue('attr_one', e.target.value)}
            />
            <input
                type="checkbox"
                checked={getValue('attr_two')}
                onChange={ e => setValue('attr_two', !!e.target.checked)}
            />
        </div>
    );
}

И внутренние функции setValue а также getValue У меня всегда есть значения по умолчанию в inputValue- Я не могу получить обновленное состояние внутри этой функции. Как я могу организовать свой код для решения этой проблемы?

PS

1) С useCallback у меня такие же результаты:

const getValue = useCallback( ( attr ) => {
    return inputValue[attr];
}, [inputValue]);
const setValue = useCallback( ( attr, val ) => {
    if( attr === 'attr_one' ) {
        if( val === 'bar' && getValue(attr) !== 'foo' ) {
            val = 'foo bar';
        }
    }
    setInputValue( {...inputValue, [attr]: val} );
}, [inputValue]);

2) С помощью функций useEffect setValue а также getValue недоступны при первом рендере:

let getValue, setValue;
useEffect( () => {
    getValue = ( attr ) => {
        return inputValue[attr];
    }
    setValue = ( attr, val ) => {
        if( attr === 'attr_one' ) {
            if( val === 'bar' && getValue(attr) !== 'foo' ) {
                val = 'foo bar';
            }
        }
        setInputValue( {...inputValue, [attr]: val} );
    }
}, [inputValue]);

2 ответа

Решение

Напишите собственные хуки, чтобы выделить вашу логику в отдельные блоки кода. Поскольку изменения вашего состояния частично зависят от предыдущего состояния, вам следует вызвать useReducer() вместо того useState() чтобы упростить реализацию и изменить состояние атомарно:

const useAccessors = initialState => {
  const [state, dispatch] = useReducer((oldState, [attr, val]) => {
    if (attr === 'attr_one') {
      if (val === 'bar' && getValue(attr) !== 'foo') {
        val = 'foo bar';
      }
    }

    // this is important! your reference must be preserved (ew)
    oldState[attr] = val;
    return oldState;
  }, initialState);

  const getValue = useCallback(
    attr => state[attr],
    [state]
  );
  const setValue = useCallback(
    (attr, val) => {
      dispatch([attr, val]);
    },
    [dispatch]
  );

  return { getValue, setValue, state };
};

Обычно я не рекомендую изменять ваш объект состояния, но в этом случае это необходимо, иначе ваш useEffect() будет очень неэффективным из-за вашего stateссылка постоянно меняется. Если кто-то знает, как обойти эту проблему, не нарушая exhaustive-deps, не стесняйтесь комментировать ниже.

Теперь твой useEffect()исключает зависимости из второго аргумента. Хотя иногда для этого есть допустимые варианты использования, обычно это просто вызывает проблемы, которые вы испытываете в настоящее время.

Давай переместим твой useEffect() в кастомный хук и исправим его:

const useFormControl = (formControl, { getValue, setValue, state }) => {
  useEffect(() => {
    if (formControl) {
      const keys = Object.keys(state);

      keys.forEach(attribute => {
        formControl.subscribeToValueCollecting(attribute, () => {
          return getValue(attribute));
        });
        formControl.subscribeToValueChange(attribute, value => {
          setValue(attribute, value);
          return true;
        });
      });

      return () => {
        keys.forEach(attribute => {
          formControl.unsubscribe(attribute);
        });
      };
    }
  }, [formControl, getValue, setValue, state]);
};

поскольку getValue а также setValue запоминаются, и state является постоянной изменяемой ссылкой, единственная зависимость, которая фактически изменяется, - это formControl, и это хорошо.

Собирая все это вместе, получаем:

const MyComponent = props =>
  const { attrOneDefault, attrTwoDefault, formControl } = props;

  const { getValue, setValue, state } = useAccessors({
    attr_one: attrOneDefault,
    attr_two: attrTwoDefault
  });

  useFormControl(formControl, { getValue, setValue, state });

  return (
    <div class="form-field">
      <input
        type="text"
        value={getValue('attr_one')}
        onChange={e => setValue('attr_one', e.target.value)}
      />
      <input
        type="checkbox"
        checked={getValue('attr_two')}
        onChange={e => setValue('attr_two', e.target.checked)}
      />
    </div>
  );
};

Попробуй это:

const getValue = ( attr ) => {
        return inputValue[attr];
    }
const getValueRef = useRef(getValue)
const setValue = ( attr, val ) => {
        setInputValue( inputValue =>{
            if( attr === 'attr_one' ) {
                if( val === 'bar' && inputValue[attr] !== 'foo' ) {
                    val = 'foo bar';
                }
            }
            return {...inputValue, [attr]: val} );
        }
}

useEffect(()=>{
    getValueRef.current=getValue
})

    useEffect( () => {
        const getCurrentValue = (attr)=>getValueRef.current(attr)
        if( formControl ) {         
            Object.keys(inputValue).forEach( attribute => {
                formControl.subscribeToValueCollecting( attribute, () => {
                    return getCurrentValue(attribute);
                });
                formControl.subscribeToValueChange( attribute, ( value ) => {
                    setValue( attribute, value );
                    return true;
                });
            });
        }

        return () => { 
            if( formControl ) {
                Object.keys(inputValue).forEach( attribute => formControl.unsubscribe(attribute) );
            }
        }
    }, []);

Другие вопросы по тегам