React Material UI TextField onchange handler continue default action - javascript

I have a MUI TextField input, that I want to do something when it changes, without preventing its native handling (without making it controlled input)
<TextField onChange={(e)=>{
something(e.target.value)
//maybe continueDefault() to make it still accept input
}}/>
How can I make it continue its default action, which is to allow it to receive and append text inputs after invoking my something() function with the data?
I wanted to allow this without having to make the input a controlled input, which is without using a state to store its value.

Have you tried your current solution? Passing just the onChange property without the value doesn't make it an controlled component, which is what you want. There is also no reason to use the hypothetical continueDefault(), since e.preventDefault() is never called.
You're current solution seems to work fine, and doesn't stop the text field from being further edited.
function something(value) {
console.log(JSON.stringify(value));
}
function App() {
return (
<MaterialUI.TextField
onChange={(e) => {
something(e.target.value);
}}
/>
);
}
const root = ReactDOM.createRoot(document.querySelector("#root"));
root.render(<App />);
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto:300,400,500,700&display=swap" />
<script crossorigin src="https://unpkg.com/react#18/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom#18/umd/react-dom.development.js"></script>
<script crossorigin src="https://unpkg.com/#mui/material#5/umd/material-ui.development.js"></script>
<div id="root"></div>

I think you shoud use a ref, and ad and event handler. Something like that (untested)
const MyComponent = () => {
const ref = useRef();
useEffect(() => {
const onChange = () => {
something(ref.current.value)
}
ref.current.addEventListerner("change", onChange)
return () => {
ref.current.removeEventListener("change", onChange)
}
})
return (
<input ref={ref} />
)
}
const ref = useRef();
useEffect(() => {
const onChange = () => {
something(ref.current.value)
}
ref.current.addEventListerner("change", onChange)
return () => ref.current.removeEventListener("change", onChange)
})
return (
<input ref={ref} />
)
}

Related

React add default value to uncontrolled MUI textfield

I'm using React 17 and Mui 5. I have lag issue on my form page because all my textfields are controlled and if I have a long form and I type very very fast on my keyboard the onChange event will be triggered a lot and it will reload the render each time because I have this.setState() inside my handleChange.
So I got an idea, it's to set all my textfields uncontrolled by removing the props value and add debounce to my handleChange function. It works but I got a problem when I'm on my edit form. I have to take the current value from json response and set this value inside my textfield but I don't have the props value anymore. So I see I can use the props defaultValue. But the problem with this props it's that it will be set just once. So at the first render I'll get empty value and at the second render (after json response) I'll get the value but the defaultValue is always empty because the second render is ignored.
Then the only way I found to make it works it's to display the textfield only when the value is set :
{this.state.my_var || hasGotJsonResponse && (
<TextField
label="Label"
name="my_var"
onChange={this.handleChange}
defaultValue={this.state.my_var}
/>
)}
It works but I don't really like to display the field only in certain condition. There is another way to solve my problem ? Or maybe there is a way to intialize defaultProps dynamically ?
Notice: I don't want to use ref at least there is an easy way to manage it on multiple input
Edit
I show you a simple snipet. Try to type very fast and you will notice the lag inside the textfield
const inputFieldKeys = Array.from({
length: 100
}).map((e, i) => "field_" + i.toString());
const App = (props) => {
const [inputState, setInputState] = React.useState({});
// this runs fine even without the `useCallback` optimization, at least for me
const handleChange = React.useCallback((e) => {
setInputState(oldState => ({ ...oldState,
[e.target.name]: e.target.value
}));
}, [setInputState]);
return ( <div>
{inputFieldKeys.map(key=>(
<MaterialUI.TextField name={key} key={key} value={inputState[key] || ""} onChange={handleChange}
label={key}/>
))}
</div>
);
};
ReactDOM.render( < App / > , document.getElementById("root"));
<script crossorigin src="https://unpkg.com/react#17/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom#17/umd/react-dom.development.js"></script>
<script crossorigin src="https://unpkg.com/#mui/material#5/umd/material-ui.production.min.js"></script>
<div id="root"></div>
The issue is each of your inputs are re-rendering when one of them changes. When you make a change to field_0 and the value is updated, since they are all sharing state, they will all re-render in the current setup.
I took your snippet and simply logged the renders. Try typing one letter in the first input, and watch all 99 other inputs re-render.
const inputFieldKeys = Array.from({
length: 100
}).map((e, i) => "field_" + i.toString());
const App = (props) => {
const [inputState, setInputState] = React.useState({});
// this runs fine even without the `useCallback` optimization, at least for me
const handleChange = React.useCallback((e) => {
setInputState(oldState => ({ ...oldState,
[e.target.name]: e.target.value
}));
}, [setInputState]);
const InputLogged = ({ name, value, onChange, label }) => {
console.log(`${name} input re-rendered`);
return (<MaterialUI.TextField name={name} key={name} value={inputState[name] || ""} onChange={handleChange}
label={name}/>);
};
return ( <div>
{inputFieldKeys.map(key=>(
<InputLogged name={key} key={key} value={inputState[key] || ""} onChange={handleChange}
label={key} />
))}
</div>
);
};
ReactDOM.render( < App / > , document.getElementById("root"));
<div id="root"></div>
<script crossorigin src="https://unpkg.com/react#17/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom#17/umd/react-dom.development.js"></script>
<script crossorigin src="https://unpkg.com/#mui/material#5/umd/material-ui.production.min.js"></script>
The key is to tell each input component when it should re-render using React memo.
Here's your snippet with some small modifications. Now each Input only re-renders when it's own value changes.
const inputFieldKeys = Array.from({
length: 100
}).map((e, i) => "field_" + i.toString());
const Input = React.memo((props) => {
console.log(`${props.field} input re-rendered`);
return (
<MaterialUI.TextField
name={props.field}
key={props.field}
value={props.value}
onChange={props.handleChange}
label={props.field}
/>
);
});
const App = (props) => {
const [inputState, setInputState] = React.useState(Object.fromEntries(inputFieldKeys.map((field) => [field, ""])));
// this runs fine even without the `useCallback` optimization, at least for me
const handleChange = React.useCallback((e) => {
setInputState(oldState => ({ ...oldState,
[e.target.name]: e.target.value
}));
}, [setInputState]);
return ( <div>
{inputFieldKeys.map((field) => {
const props = { field, handleChange, value: inputState[field] };
return (
<Input {...props} />
)
})}
</div>
);
};
ReactDOM.render( < App / > , document.getElementById("root"));
<div id="root"></div>
<script crossorigin src="https://unpkg.com/react#17/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom#17/umd/react-dom.development.js"></script>
<script crossorigin src="https://unpkg.com/#mui/material#5/umd/material-ui.production.min.js"></script>
Take a look at this sandbox demonstration I created that helps better understand memo and how it pertains to component re-rendering when state changes.
Additionally, if you must use a class component, memoization is simply handled using PureComponent. This implements componentDidUpdate under-the-hood which does a shallow comparison of props and only re-renders when they change.
Simply use:
class Greeting extends PureComponent {}

Parent's methods in array dependencies in useCallback

Let say we have parent and children components like this:
const {useState, useCallback} = React;
const ComponentB = (props) => {
const [text, setText] = useState('')
const { onClick } = props
const handleChange = useCallback((event) => {
setText(event.target.value)
}, [text])
const handleClick = useCallback(() => {
onClick(text)
}, [onClick, text]) // Should I to take into account 'onClick' props?
return (
<div>
<input type="text" onChange={ handleChange } />
<button type="button" onClick={ handleClick }>Save</button>
</div>
)
}
const ComponentA = () => {
const [stateA, setStateA] = useState('')
const handleSetStateA = useCallback((state) => {
setStateA(state)
}, [stateA])
return (
<div>
<ComponentB onClick={ handleSetStateA } />
{ stateA && `${ stateA } saved!` }
</div>
)
}
ReactDOM.createRoot(
document.getElementById("root")
).render(
<ComponentA />
);
<div id="root"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.1.0/umd/react.development.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.1.0/umd/react-dom.development.js"></script>
React documentation says that:
every value referenced inside the callback should also appear in the dependencies array
And I'm wondering if I need to put onClick method to array dependencies in useCallback? And if so, why I should do that?
And I'm wondering if I need to put onClick method to array dependencies in useCallback?
Yes.
And if so, why I should do that?
Because it's possible that your component will get re-rendered with a new and different function for the onClick prop that behaves differently from the old one. Without saying it's a dependency, you'll continue using the old value.
In fact, in your given code, it's not just possible but definite: you create a new handleSetStateA function every time stateA changes.
That said, in ComponentA:
There's no reason to have stateA as a dependency in your useCallback creating handleSetStateA; handleSetStateA never uses stateA. (It uses the state setter function for it, but that's not the same thing.)
There's not really any reason for handleSetStateA at all; just pass setStateA directly as onClick. But I'm assuming you do more than just setting the state in that function and just left things out for the purposes of the question.
(Similarly, in ComponentB there's no reason for text to be a dependency on the useCallback for handleChange; handleChange doesn't use text.)
But even if you change ComponentA to pass setStateA directly (or at least provide a stable function), ComponentB shouldn't rely on onClick being unchanging between renders, so you'd use onClick in your useCallback dependency list regardless.
Finally: There's not much point in using useCallback with functions you're passing to unmemoized components. For it to be useful, the component you're providing the callback function to should be memoized (for instance, via React.memo or, for a class component, via shouldComponentUpdate). See my answer here for details on that.
Here's an updated version of your snippet using React.memo and only the necessary dependencies; I've left handleSetStateA in (I added a console.log so it isn't just a wrapper):
const { useState, useCallback } = React;
const ComponentB = React.memo(({ onClick }) => {
const [text, setText] = useState("");
const handleChange = useCallback((event) => {
setText(event.target.value);
}, []);
const handleClick = useCallback(() => {
console.log(`Handling click when text = "${text}"`);
onClick(text);
}, [onClick, text]);
return (
<div>
<input type="text" onChange={handleChange} />
<button type="button" onClick={handleClick}>
Save
</button>
</div>
);
});
const ComponentA = () => {
const [stateA, setStateA] = useState("");
const handleSetStateA = useCallback((state) => {
console.log(`Setting stateA to "${state}"`);
setStateA(state);
}, []);
return (
<div>
<ComponentB onClick={handleSetStateA} />
{stateA && `${stateA} saved!`}
</div>
);
};
ReactDOM.createRoot(document.getElementById("root")).render(<ComponentA />);
<div id="root"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.1.0/umd/react.development.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.1.0/umd/react-dom.development.js"></script>

React - functional component does not return updated variable values

I have a class Component FooPage which renders an imported functional Component BarForm. I pass handleSubmit as a prop to BarForm and want to capture the inputData value. The issue is the value is returned as the initial value.
To help debugging: before I submit, the latest value of inputData was logged correctly, and the submit did not trigger re-rendering. Still, FooPage console logs "initial value". What's more, changing onSubmit callback from handleSubmit(inputData) to arrow function (event) => handleSubmit(inputData)(event) fixes the problem.
While I know I can achieve this by useState, I want to understand why the updated inputData is returned as initial value even without re-rendering, and why using arrow function as callback fixed the problem.
const BarForm = ({ handleSubmit }) => {
let inputData = "initial value"
console.log("I just re-rendered.") //Check BarForm did not re-render when submit.
return (
<form onSubmit={ handleSubmit(inputData) }> {/* won't work, but arrow function does */}
<input
onChange = {(event) => {
inputData = event.target.value; //update inputData
console.log(inputData); //confirm the value of inputData before submit
}}
/>
<button type="submit">Submit</button> {/* But */}
</form>
)
}
class FooPage extends React.Component {
handleSubmit = (inputData) => (event) => {
event.preventDefault(); //Don't re-render
console.log(inputData); //See the output
}
render() {
return (
<BarForm handleSubmit = {this.handleSubmit} />
)
}
}
ReactDOM.render(<FooPage />, document.getElementById("app"));
<script src="https://unpkg.com/react#16/umd/react.production.min.js" crossorigin></script>
<script src="https://unpkg.com/react-dom#16/umd/react-dom.production.min.js" crossorigin></script>
<div id="app"></div>
When you set <form onSubmit={ handleSubmit(inputData) }>, what you are actually doing in this case is calling the handleSubmit from FooPage function which returns a new function with inputData as captured when the component was first rendered as part of the function's scope.
When you call it with the arrow function, you don't actually create a new function when the component renders. You create the function onSubmit.

How can I make this react input component idiomatic?

Here's a react input component:
function Input({ value, setValue }) {
return (
<div>
<input value={value} onChange={event => setValue(event.target.value)} />
<button onClick={() => setValue(value.toUpperCase())}>Capitalize</button>
</div>
);
}
It's just a vanilla input component together with a button that capitalizes the input's value. It's meant to be controlled by some parent component:
function Parent() {
let [value, setValue] = useState("");
return <Input value={value} setValue={setValue} />;
}
This works fine, but it's not idiomatic. To be useable as a "drop-in replacement" for a vanilla input component, it should take an onChange prop, not setValue, the relevant difference being that onChange takes a synthetic event as an argument while setValue takes a string. (I'd like the presence of the capitalize button to be "opaque" to a developer using this Input component.)
I tried to idiomaticize this (see snippet below) by having the input element fire off a change event when the button is clicked, but the this doesn't cause onChange to execute. (I assume that this is due to details of react's synthetic event system that I don't understand. I browsed a bunch of posts on this topic, but couldn't get the ideas I found to work.)
function AnotherInput({ value, onChange }) {
let input = useRef();
let handleClick = () => {
input.current.value = value.toUpperCase();
var event = new Event("change" /* also tried "input" */, {
bubbles: true
});
input.current.dispatchEvent(event); // onChange doesn't fire!
};
return (
<div>
<input value={value} ref={input} onChange={onChange} />
<button onClick={handleClick}>Capitalize</button>
</div>
);
}
Also, I feel I shouldn't have to use a ref here because I don't want to modify the DOM directly; I just want to change the value in the controlling parent component.
Here's a CodePen.
I made it work by simulating the event Object on the Capitalize Button.
Parent Component:
function Parent() {
let [value, setValue] = useState("");
return <Input value={value} onChange={(e) => setValue(e.target.value)} />;
}
Input Component:
EDITED: I've managed to came up with a more elegant solution to the Input Component:
function Input({ value, onChange: inheritedOnChange }) {
return (
<div>
<input value={value} onChange={inheritedOnChange} />
<button value={value.toUpperCase()} onClick={inheritedOnChange}>Capitalize</button>
</div>
);
}
Note that i renamed the onChange prop to inheritedOnChange just for readability purposes. Preserving the onChange name at the destructuring should still work.

Input onChange() event is not called

I'm rendering an Input of type='number'.
The Input has the value of this.state.value.
The Input and all the UI Components are generated via Semantic-UI, but I think that's not of a significant importance info.
I also have a custom arrow menu for this input instead of the original one. [input of type number has two arrows to decrease/increase the value]
Render()
render() {
// Custom Menu
const arrowsMenu = (
<Menu compact size='tiny'>
<Menu.Item as='a' onClick={ this.decreaseNumber.bind(this) }>
<Icon name='chevron left' size='small' />
</Menu.Item>
<Menu.Item as='a' onClick={ this.increaseNumber.bind(this) }>
<Icon name='chevron right' size='small' />
</Menu.Item>
</Menu>
);
return (
<Input value={ this.state.value } type="number" label={ arrowsMenu } placeholder="Raplece ma" onChange={ this.onChange.bind(this) } />
);
}
The Custom Menu uses these two functions:
decreaseNumber(e) {
this.setState({
value: this.state.value - 1
});
}
increaseNumber(e) {
this.setState({
value: this.state.value + 1
});
}
onChange
You can place anything.
onChange(e) {
console.log('====================================');
console.log('Hello pals');
console.log('====================================');
}
The problem is
That whenever I push an Arrow from the Menu, the onChange() event of the Input is not triggered. But the value of the input is changed.
(Of course, because the this.state.value variable is changed in the state)
If I do the same with the original arrows, of course, the value is changed as it should.
Why is that and how can I fix it?
onChange is only called if the user goes into the Input component and interacts with it to change the value (e.g. if they type in a new value). onChange is not called if you change the value programmatically through some other avenue (in your example changing it via the custom menu).
This is working as intended design.
If you want to trigger onChange, then call it from your increaseNumber and decreaseNumber methods.
You can call onChange with any code you want, but if you want to reflect the new value you need to set the state according to the new input value from the event.
As for decreaseNumber and increaseNumber you need to change the state as well but here you are doing calculation so you need to make sure it's a number (or convert it to a number) because you are getting a string back from the event.
Working example:
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
value: 0
};
}
onChange = e => this.setState({value: e.target.value});
increaseNumber = () => this.setState({value: Number(this.state.value) + 1});
decreaseNumber = () => this.setState({ value: Number(this.state.value) - 1 });
render() {
const { value } = this.state;
return (
<div>
<button onClick={this.decreaseNumber}>-</button>
<input type="number" value={value} onChange={this.onChange}/>
<button onClick={this.increaseNumber}>+</button>
</div>
);
}
}
ReactDOM.render(<App />, document.getElementById("root"));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.min.js"></script>
<div id="root"></div>
Edit
For triggering the onChange handler just call this.onChange but note that you can't pass the event like the native event handler does but you can pass a simple object that mimic the normal event object with a target.value.
Another option is to try triggering it via a ref but keep in mind it can cause an infinite loop in some cases.
Edited Example:
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
value: 0
};
}
onChange = (e) => {
this.setState({ value: e.target.value });
console.log('change', e.target.value);
}
increaseNumber = () => {
const { value } = this.state;
const nextValue = Number(value) + 1;
const changeEvent = {
target: {
value: nextValue
}
};
this.onChange(changeEvent);
}
decreaseNumber = () => {
const { value } = this.state;
const nextValue = Number(value) - 1;
const changeEvent = {
target: {
value: nextValue
}
};
this.onChange(changeEvent);
}
render() {
const { value } = this.state;
return (
<div>
<button onClick={this.decreaseNumber}>-</button>
<input type="number" value={value} onChange={this.onChange} />
<button onClick={this.increaseNumber}>+</button>
</div>
);
}
}
ReactDOM.render(<App />, document.getElementById("root"));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.min.js"></script>
<div id="root"></div>
Edit #2
As a followup to your comment:
list's value is its content/children. If some of the children change,
then list changes with them as well
Well, this has an easy solution, you can use the ref (like i mentioned in the first section of my answer) and dispatch an event with bubbles:true so it will bubble all the way up to the parents.
Using your new example code:
class App extends React.Component {
liOnChange(e) {
console.log('listed item/items changed');
}
inputOnChange(e) {
console.log('input changed');
}
handleClick(e){
var event = new Event("input", { bubbles: true });
this.myInput.dispatchEvent(event);
}
render() {
return(
<div>
<ul>
<li onChange={this.liOnChange.bind(this)}>
<input ref={ref => this.myInput = ref} type='text'onChange={this.inputOnChange.bind(this)}/>
<button onClick={this.handleClick.bind(this)}>+</button>
</li>
</ul>
</div>
);
}
}
ReactDOM.render(<App />, document.getElementById("root"));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.min.js"></script>
<div id="root"></div>
I in general don't like using refs but sometimes you need them.

Categories