最新消息:雨落星辰是一个专注网站SEO优化、网站SEO诊断、搜索引擎研究、网络营销推广、网站策划运营及站长类的自媒体原创博客

javascript - React Hooks: accessing state across functions without changing event handler function references - Stack Overflow

programmeradmin1浏览0评论

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.

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.

Share Improve this question edited Feb 8, 2019 at 18:22 asleepysamurai asked Feb 8, 2019 at 17:31 asleepysamuraiasleepysamurai 1,3722 gold badges14 silver badges23 bronze badges 5
  • i guess you have to bind the change handler inside the constructor of your component – messerbill Commented Feb 8, 2019 at 17:36
  • There are no constructors when using hooks. Hooks can only be used inside function components in React. – asleepysamurai Commented Feb 8, 2019 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 Commented Feb 8, 2019 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 Commented Feb 8, 2019 at 17:40
  • 1 Possible duplicate of What useCallback do in React? – messerbill Commented Feb 8, 2019 at 17:45
Add a comment  | 

4 Answers 4

Reset to default 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.

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

与本文相关的文章

发布评论

评论列表(0)

  1. 暂无评论