Avoiding Unnecessary Renders

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