In a class based React component I do something like this:
class SomeComponent extends React.Component{
onChange(ev){
this.setState({text: ev.currentValue.text});
}
transformText(){
return this.state.text.toUpperCase();
}
render(){
return (
<input type="text" onChange={this.onChange} value={this.transformText()} />
);
}
}
This is a bit of a contrived example to simplify my point. What I essentially want to do is maintain a constant reference to the onChange function. In the above example, when React re-renders my component, it will not re-render the input if the input value has not changed.
Important things to note here:
- this.onChange is a constant reference to the same function.
- this.onChange needs to be able to access the state setter (in this case this.setState)
Now if I were to rewrite this component using hooks:
function onChange(setText, ev) {
setText(ev.currentValue.text);
};
function transformText(text) {
return text.toUpperCase();
};
function SomeComponent(props) {
const [text, setText] = useState('');
return (
<input type="text" onChange={onChange} value={transformText()} />
);
}
The problem now is that I need to pass text
to transformText
and setText
to onChange
methods respectively. The possible solutions I can think of are:
- Define the functions inside the component function, and use closures to pass the value along.
- Inside the component function, bind the value to the methods and then use the bound methods.
Doing either of these will change the constant reference to the functions that I need to maintain in order to not have the input
component re-render. How do I do this with hooks? Is it even possible?
Please note that this is a very simplified, contrived example. My actual use case is pretty complex, and I absolutely don't want to re-render components unnecessarily.
Edit:
This is not a duplicate of What useCallback do in React? because I'm trying to figure out how to achieve a similar effect to what used to be done in the class component way, and while useCallback
provides a way of doing it, it's not ideal for maintainability concerns.
In a class based React component I do something like this:
class SomeComponent extends React.Component{
onChange(ev){
this.setState({text: ev.currentValue.text});
}
transformText(){
return this.state.text.toUpperCase();
}
render(){
return (
<input type="text" onChange={this.onChange} value={this.transformText()} />
);
}
}
This is a bit of a contrived example to simplify my point. What I essentially want to do is maintain a constant reference to the onChange function. In the above example, when React re-renders my component, it will not re-render the input if the input value has not changed.
Important things to note here:
- this.onChange is a constant reference to the same function.
- this.onChange needs to be able to access the state setter (in this case this.setState)
Now if I were to rewrite this component using hooks:
function onChange(setText, ev) {
setText(ev.currentValue.text);
};
function transformText(text) {
return text.toUpperCase();
};
function SomeComponent(props) {
const [text, setText] = useState('');
return (
<input type="text" onChange={onChange} value={transformText()} />
);
}
The problem now is that I need to pass text
to transformText
and setText
to onChange
methods respectively. The possible solutions I can think of are:
- Define the functions inside the component function, and use closures to pass the value along.
- Inside the component function, bind the value to the methods and then use the bound methods.
Doing either of these will change the constant reference to the functions that I need to maintain in order to not have the input
component re-render. How do I do this with hooks? Is it even possible?
Please note that this is a very simplified, contrived example. My actual use case is pretty complex, and I absolutely don't want to re-render components unnecessarily.
Edit:
This is not a duplicate of What useCallback do in React? because I'm trying to figure out how to achieve a similar effect to what used to be done in the class component way, and while useCallback
provides a way of doing it, it's not ideal for maintainability concerns.
4 Answers
Reset to default 7This is where you can build your own hook (Dan Abramov urged not to use the term "Custom Hooks" as it makes creating your own hook harder/more advanced than it is, which is just copy/paste your logic) extracting the text transformation logic
Simply "cut" the commented out code below from Mohamed's answer.
function SomeComponent(props) {
// const [text, setText] = React.useState("");
// const onChange = ev => {
// setText(ev.target.value);
// };
// function transformText(text) {
// return text.toUpperCase();
// }
const { onChange, text } = useTransformedText();
return (
<input type="text" onChange={React.useCallback(onChange)} value={text} />
);
}
And paste it into a new function (prefix with "use*" by convention). Name the state & callback to return (either as an object or an array depending on your situation)
function useTransformedText(textTransformer = text => text.toUpperCase()) {
const [text, setText] = React.useState("");
const onChange = ev => {
setText(ev.target.value);
};
return { onChange, text: textTransformer(text) };
}
As the transformation logic can be passed (but uses UpperCase by default), you can use the shared logic using your own hook.
function UpperCaseInput(props) {
const { onChange, text } = useTransformedText();
return (
<input type="text" onChange={React.useCallback(onChange)} value={text} />
);
}
function LowerCaseInput(props) {
const { onChange, text } = useTransformedText(text => text.toLowerCase());
return (
<input type="text" onChange={React.useCallback(onChange)} value={text} />
);
}
You can use above components like following.
function App() {
return (
<div className="App">
To Upper case: <UpperCaseInput />
<br />
To Lower case: <LowerCaseInput />
</div>
);
}
Result would look like this.
You can run the working code here.
Define the callbacks inside the component function, and use closures to pass the value along. Then what you are looking for is useCallback
hook to avoid unnecessary re-renders. (for this example, it's not very useful)
function transformText(text) {
return text.toUpperCase();
};
function SomeComponent(props) {
const [text, setText] = useState('');
const onChange = useCallback((ev) => {
setText(ev.target.value);
}, []);
return (
<input type="text" onChange={onChange} value={transformText(text)} />
);
}
Read more here
I know it's bad form to answer my own question but based on this reply, and this reply, it looks like I'll have to build my own custom hook to do this.
I've basically built a hook which binds a callback function with the given arguments and memoizes it. It only rebinds the callback if the given arguments change.
If anybody would find a need for a similar hook, I've open sourced it as a separate project. It's available on Github and NPM.
The case isn't specific to hooks, it would be the same for class component and setState
in case transformText
and onChange
should be extracted from a class. There's no need for one-line functions to be extracted, so it can be assumed that real functions are complex enough to justify the extraction.
It's perfectly fine to have transform function that accepts a value as an argument.
As for event handler, it should have a reference to setState
, this limits ways in which it can be used.
A common recipe is to use state updater function. In case it needs to accept additional value (e.g. event value), it should be higher-order function.
const transformText = text => text.toUpperCase();
const onChange = val => _prevState => ({ text: val });
function SomeComponent(props) {
const [text, setText] = useState('');
return (
<input type="text" onChange={e => setText(onChange(e.currentValue.text)} value={transformText(text)} />
);
}
This recipe doesn't look useful in this case because original onChange
doesn't do much. This also means that the extraction wasn't justified.
A way that is specific to hooks is that setText
can be passed as a callback, in contrast to this.setState
. So onChange
can be higher-order function:
const transformText = text => text.toUpperCase();
const onChange = setState => e => setState({ text: e.currentValue.text });
function SomeComponent(props) {
const [text, setText] = useState('');
return (
<input type="text" onChange={onChange(setText)} value={transformText(text)} />
);
}
If the intention is to reduce re-renders of children caused by changes in onChange
prop, onChange
should be memoized with useCallback
or useMemo
. This is possible since useState
setter function doesn't change between component updates:
...
function SomeComponent(props) {
const [text, setText] = useState('');
const memoizedOnChange = useMemo(() => onChange(setText), []);
return (
<input type="text" onChange={memoizedOnChange} value={transformText(text)} />
);
}
The same thing can be achieved by not extracting onChange
and using useCallback
:
...
function SomeComponent(props) {
const [text, setText] = useState('');
const onChange = e => setText({ text: e.currentValue.text });
const memoizedOnChange = useCallback(onChange, []);
return (
<input type="text" onChange={memoizedOnChange} value={transformText(text)} />
);
}
bind
the change handler inside the constructor of your component – messerbill Commented Feb 8, 2019 at 17:36