A 'ref' can be used to prevent excessive recreations of the callback passed to useCallback while still receiving the updated value of a variable in the callback. |
const ExpensiveTree = React.memo(props=>{ console.log('rendering'); return (<button onClick={props.onSubmit}>SUBMIT</button>);});function Example() { const [text, updateText] = React.useState(''); const textRef = React.useRef(); React.useEffect(() => { textRef.current = text; // Write it to the ref }); const handleSubmit = React.useCallback(() => { const currentText = textRef.current; // Read it from the ref alert(currentText); }, [textRef]); // Don't recreate handleSubmit like [text] would do return ( <React.Fragment> <input value={text} onChange={e => updateText(e.target.value)} /> <ExpensiveTree onSubmit={handleSubmit} /> </React.Fragment> );}ReactDOM.render(<Example/>,document.querySelector("div")); |
|
const handleSubmit = () => { alert(text);}); |
Bad! This would cause <ExpensiveTree> to be rendered on every key press or any state change, as the child receives a different copy of the event handler through props. |
const handleSubmit = React.useCallback(() => { alert(text);}, [text]); |
Bad! This would cause <ExpensiveTree> to be rendered on every key press as 'text' changes. |
const handleSubmit = React.useCallback(() => { alert(text);}, []); |
Bad! This would cause the event handler to always get the initial value of 'text', ie. empty string. |
const handleSubmit = React.useCallback(() => { const currentText = textRef.current; alert(currentText);}, []); |
This is fine too as long as we use the same copy of 'ref'. |
A fanciful way to encapsulate the functionality above is to write a custom hook (Chapter 22): |
const ExpensiveTree = React.memo(props=>{ console.log('rendering'); return (<button onClick={props.onSubmit}>SUBMIT</button>);});function Example() { const [text, updateText] = React.useState(''); const handleSubmit = useEventCallback(() => { alert(text); }, [text]); return ( <React.Fragment> <input value={text} onChange={e => updateText(e.target.value)} /> <ExpensiveTree onSubmit={handleSubmit} /> </React.Fragment> );}function useEventCallback(fn, dependencies) { const ref = React.useRef(() => { throw new Error('Cannot call an event handler while rendering.'); }); React.useEffect(() => { ref.current = fn; }, [fn, ...dependencies]); return React.useCallback(() => { const fn = ref.current; return fn(); }, [ref]);}ReactDOM.render(<Example/>,document.querySelector("div")); |
A still better way is to pass the update function down a context: |
const myContext = React.createContext();const ExpensiveTree = React.memo(props=>{ console.log('rendering'); const ut = React.useContext(myContext); return (<button onClick={()=>{ut(t=>{alert(t); return t;})}}>SUBMIT</button>);});function Example() { const [text, updateText] = React.useState(''); return ( <myContext.Provider value={updateText}> <React.Fragment> <input value={text} onChange={e => updateText(e.target.value)} /> <ExpensiveTree /> </React.Fragment> </myContext.Provider> );}ReactDOM.render(<Example/>,document.querySelector("div")); |