13

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:

  1. this.onChange is a constant reference to the same function.
  2. 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:

  1. Define the functions inside the component function, and use closures to pass the value along.
  2. 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.

asleepysamurai
  • 1,362
  • 2
  • 14
  • 23
  • i guess you have to `bind` the change handler inside the constructor of your component – messerbill Feb 08 '19 at 17:36
  • There are no constructors when using hooks. Hooks can only be used inside function components in React. – asleepysamurai Feb 08 '19 at 17:38
  • yes but you do not need to make use of those hooks imho....just take your class based component and add the binding to the constructor – messerbill Feb 08 '19 at 17:39
  • Ah yes, I can always do that. But I'm trying to understand if hooks can handle this particular scenario, and if I'm doing it incorrectly. Also, in this particular case, if I'm using classes, I don't actually need to bind anything. – asleepysamurai Feb 08 '19 at 17:40
  • 1
    Possible duplicate of [What useCallback do in React?](https://stackoverflow.com/questions/53159301/what-usecallback-do-in-react) – messerbill Feb 08 '19 at 17:45

4 Answers4

7

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

Mohamed Ramrami
  • 12,026
  • 4
  • 33
  • 49
  • Great! That does indeed work. Now is it possible to do the same without having `onChange` and `transformText` defined inside the function component? I'm going to have a lot of these functions, and it would be much more maintainable to split them out into separate functions. – asleepysamurai Feb 08 '19 at 17:53
  • It seems like then you should really just put them in a separate folder and import them then. Thus you can still use the hook to set the state of the functional component but keep your functions outside for maintainability. – ekr990011 Feb 08 '19 at 18:04
  • @ekr990011 If I do that, the closure will be broken and `setText` will be undefined inside `onChange`. – asleepysamurai Feb 08 '19 at 18:14
  • 1
    for `useCallback`~~~ – dance2die Feb 08 '19 at 18:25
  • `useCallback` should be on top: https://reactjs.org/docs/hooks-rules.html#only-call-hooks-at-the-top-level – tokland Sep 04 '19 at 20:57
7

This 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.

result demo

You can run the working code here.
Edit so.answer.54597527

dance2die
  • 35,807
  • 39
  • 131
  • 194
  • 1
    Yes I believe `Custom Hooks` are the way to go for this. Only instead of having a `useTextTransform` hook, I'm going to go with a `useBoundCallback` hook, which kinda does what `useCallback` does but instead of memoizing the passed in callback, it binds the passed in args and memoizes that, such that if the passed in args are different, then the callback is rebound. This is more generic and allows me to reuse wherever I need a bound callback. – asleepysamurai Feb 08 '19 at 18:25
  • @asleepysamurai Sounds like a good idea. If `useBoundCallback` (not sure how it's implemented) works in your situation, you should use whatever works – dance2die Feb 08 '19 at 18:27
  • 1
    If you're wondering [this](https://github.com/asleepysamurai/react-use-bound-callback-hook/blob/master/index.js) is how it's implemented. – asleepysamurai Feb 08 '19 at 20:14
  • That is nice :) – Mohamed Ramrami Feb 08 '19 at 20:55
  • Agreed, @Mohamed Ramrami . Very nice – dance2die Feb 08 '19 at 21:18
  • 2
    What is `text` needs to be accessed within more functions, and those cannot be placed within the `useTransformedText` hook? when passing the `text` as a parameter to those functions, they cannot see the changes in it... – vsync Jun 16 '19 at 09:42
  • That's a good question, @vsync. You can pass `text` elsewhere as it's a state tracked by React. When a change is made from `input` via `onChange`, passing that `text` to another function/component would see the updated value. Check out this sandbox - https://codesandbox.io/s/soanswer54597527-for-comment9980901654598211-dp4fc – dance2die Jun 16 '19 at 12:57
0

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)} />
    );
}
Estus Flask
  • 206,104
  • 70
  • 425
  • 565
  • Like I said, this is a contrived simplified example. The actual methods I want to extract are much more complicated. And I do have justifiable performance reasons for wanting to not change the reference to the event handler method. Which is what using an anonymous function like your answer suggests does. The method returned by `onChange(setText)` will differ on every render and therefore the `input` component will be re-rendered over and over again (which is not acceptable to me for performance reasons). – asleepysamurai Feb 08 '19 at 19:58
  • Did you do measurements to be sure this will cause performance issues? I'd not expect it to be that bad. `useCallback` can be used for that, as you already noted. If there are too many unneeded rerenders you likely need to prevent them in the first place and make the component pure. But again, you don't have to do optimizations without being sure that they really benefit the performance. – Estus Flask Feb 08 '19 at 20:12
  • Yep, there are a huge number of child components to which these bound callbacks get passed. In the order of thousands. So it ends up causing a very noticable performance impact. – asleepysamurai Feb 08 '19 at 20:17
  • I see. Then use useCallback or useMemo. I updated the code. – Estus Flask Feb 08 '19 at 21:13
  • useCallback and useMemo both require the callback to be defined inside the function component, which makes maintaining the code a bit difficult. That's why I created a custom hook and am using that instead. – asleepysamurai Feb 08 '19 at 22:06
  • In useMemo example onChange is defined outside the component. I'm not sure what maintenance problems refer to. If you mean that it results in boilerplate code then yes, as with any other WET code you can extract common functionality to helper function (which custom hook basically is). – Estus Flask Feb 08 '19 at 22:24
0

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.

asleepysamurai
  • 1,362
  • 2
  • 14
  • 23