Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

React Hooks: accessing state across functions without changing event handler function references

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.

like image 240
asleepysamurai Avatar asked Feb 08 '19 17:02

asleepysamurai


People also ask

What is the usestate hook in react?

Line 1: We import the useState Hook from React. It lets us keep local state in a function component. Line 4: Inside the Example component, we declare a new state variable by calling the useState Hook. It returns a pair of values, to which we give names.

How to manage state on functional components in react?

There are many methods of managing state in React, including class-based state management and third-party libraries like Redux. In this tutorial, you’ll manage state on functional components using a method encouraged by the official React documentation: Hooks. Hooks are a broad set of tools that run custom functions when a component’s props change.

Why can’t my react event listener access the latest state?

If you’re using React hooks in a component with an event listener, your event listener callback cannot access the latest state. We can incorporate useRef to solve this problem. In this simple example, we are trying to access our state on a double-click event:

How do you handle events in react JS?

Handling Events. Handling events with React elements is very similar to handling events on DOM elements. There are some syntactic differences: React events are named using camelCase, rather than lowercase. With JSX you pass a function as the event handler, rather than a string.


2 Answers

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

like image 182
dance2die Avatar answered Oct 15 '22 15:10

dance2die


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

like image 25
Mohamed Ramrami Avatar answered Oct 15 '22 13:10

Mohamed Ramrami