2

In my application I have component like that:

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>
    );
}

And inside functions setValue and getValue I always have default values in inputValue - I can't get updated state inside this functions. How i can organize my code to solve this problem?

P. S.

1) With useCallback I have the same results:

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) With useEffect functions setValue and getValue are unavailable at first render:

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]);
4
  • Why does your setValue() have seemingly random logic in it? Commented Dec 20, 2019 at 16:34
  • It's just example - this function has some logic. I wanted to make the code shorter and easier to understand. Commented Dec 20, 2019 at 16:36
  • Your useEffect is capturing your inputValue, that's why it's always the same. Try passing inputValue into your use effect array [] Commented Dec 20, 2019 at 16:45
  • use a Ref callback for formControl.subscribe... listeners Commented Dec 20, 2019 at 16:47

2 Answers 2

2

Write custom hooks to extract your logic into separate units of code. Since your state changes rely in part on the previous state, you should call useReducer() instead of useState() to make the implementation easier and the state changes atomic:

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

    return { ...prev, [attr]: val };
  }, initialState);
  const ref = useRef(state);

  useEffect(() => {
    ref.current = state;
  }, [ref]);

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

  return { getValue, setValue, ref };
};

Now your useEffect() is omitting dependencies from the second argument. This tends to cause problems like you're currently experiencing. We can employ useRef() to work around this.

Let's move your useEffect() into a custom hook as well and fix it:

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

      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, ref]);
};

Since getValue, setValue, and ref are memoized, the only dependency that actually changes is formControl, which is good.

Putting all this together, we get:

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

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

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

  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>
  );
};
Sign up to request clarification or add additional context in comments.

4 Comments

@AlexShul I have to ask, did you implement those subscribeToValueCollecting() and subscribeToValueChange() methods? If not, what are they really called, and what library do they come from? That could potentially help me to avoid mutating the object in order to achieve reasonable performance.
Yes, I implement this methods in custom hook useFormControl. This hook helps validate forms and submit data. It passed from parent component. Hook code here: github.com/alex-shul/custom-components/blob/master/hook/Form.js
Any suggestions or commits for performance improvement are welcome.
@AlexShul I know it's been a long time but I just saw this answer again recently and thought it deserved an update. There's no need to mutate your state in order to get good performance, this is what useRef() is for.
0

Try this:

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) );
            }
        }
    }, []);

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.