React add default value to uncontrolled MUI textfield - javascript

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 {}

Related

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 Material UI TextField onchange handler continue default action

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

I am trying to add elements to object using usestate in React?

I am trying to add a new fields in the useState hook
// prevstate
let [currentdata, setcurrentdata] = useState({
id:'',
name:''
})
I try to add new fields in the object like that
setcurrentdata(currentdata => ({
...currentdata,
q: quantity,
}))
but it did not add new fields
The code snippet below may be one possible solution to achieve the desired objective.
NOTE: I personally dislike the idea of using useState in conjunction with such objects & prefer multiple simpler useState on each variable instead.
Code Snippet
const {useState} = React;
const MyFunction = () => {
const [badStateObj, setBadStateObj] = useState({
id: '',
name: ''
});
const clickHandler = (propName) => setBadStateObj(prev => ({
...prev,
[propName]: prev[propName] || ''
}));
const updateHandler = (propName, propVal) => setBadStateObj(prev => ({
...prev,
[propName]: propVal
}));
return (
<div>
{Object.entries(badStateObj).map(([k,v]) => (
<div>
Key: {k}   Value: {v}    
<button
onClick={() => updateHandler(k, prompt(
`Enter new value for ${k}`
))}
>
Update {k}
</button>
</div>
))}
<button
onClick={() => clickHandler(prompt('Enter prop name'))}
>
Add new prop
</button>
</div>
);
};
ReactDOM.render(
<div>
<h4>Demo showing the useState that I personally dislike</h4>
<MyFunction />
</div>,
document.getElementById("react")
);
<div id="react"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.1/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.1/umd/react-dom.production.min.js"></script>
Explanation
The badStateObj is initialized with only id and name props
When user clicks the Add new prop button, a new prop may be added
Separate buttons exist to update the value of each individual prop
clickHandler adds the new prop entered by user with '' value.
updateHandler accepts propName and propVal as parameters and updates the appropriate props within the badStateObj.

how can i pass functional react component to a function?

so what I am trying to achieve here is storing a whole component in an array in a parent component which renders a specific component in the array using its index for example :
export const Test = () => {
const [components, setComponents] = useState([
<Order key={1} />,
<Order key={2} />,
<Order key={3} />,
]);
const [index, setIndex] = useState(0);
return (
<div>
<button onClick={() => setIndex((old) => (old + 1) % components.length)}>
change
</button>
{`page ` + index}
{components[index]}
</div>
);
};
const Order = () => {
const [someState, setSomeState] = useState(1);
return (
<div>
<button onClick={() => setSomeState((old) => old + 1)}>
{someState}
</button>
</div>
);
};
when I change the state of one item then cycle through the items then return to the item which I changed its state i found that it is not updated
what I figured out is that the component in the array (in the Test component) doesn't get updated and I couldn't figure out how to update it
what I don't want to do is storing the state of the order item in the parent and pass it as props (because it will be a pain to make it work)
const App = ({ flag }) => {
if (flag) <Order />
return null
}
I'm giving you an example so i can explain what might happen in your case. If the flag becomes false from a true, the App turns blank. But what happen to the Order? It's unmounted, why? Since when React compares between the previous scene and the current scene, it notice there's no such Order any more. So what you think about the memory of component of Order (which is called a fiber)?
I guess the answer is, the memory goes to be deleted and will be collected for future use.
Now back to your case, you are using an id to switch to different component. But in theory it should behave very similar to my example for each component.
NOTE: the take away is that if you want to use an array, that's fine, but all components has to be rendered at ALL time, you can hide them, but you can't unmount any of them.
what I don't want to do is storing the state of the order item in the
parent and pass it as props (because it will be a pain to make it
work)
Your problem is that when you render a Test component and then increase index, then you render another Test component with a different key, so reacts reconciliation algorithm unmounts the old one and you lose the state.
You have two options:
lift state of each Test component up, then when one gets unmounted, you will remount it with the old state, because state will be stored in parent, it will not be lost
another option is to render all components and only show those which you want using CSS display property, this way none of them gets unmounted and you retain state. Here is example:
const Order = () => {
const [someState, setSomeState] = React.useState(1);
return (
<div>
<button onClick={() => setSomeState((old) => old + 1)}>
{someState}
</button>
</div>
);
};
let components = [<Order />, <Order />, <Order />];
const Test = () => {
const [index, setIndex] = React.useState(0);
return (
<div>
<button onClick={() => setIndex((old) => (old + 1) % components.length)}>
change
</button>
{`page ` + index}
{[0, 1, 2].map((x) => (
<div key={x} style={{ display: index === x ? "block" : "none" }}>
{components[x]}
</div>
))}
</div>
);
};
ReactDOM.render(
<Test />,
document.getElementById("react")
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.1/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.1/umd/react-dom.production.min.js"></script>
<div id="react"></div>
PS I have removed components from state, I can't find official info now, but IMHO it is not good idea to store components in state.

React - How to prevent re-rendering of all the input fields when input changes

I am implementing a form which is generated using a Json. The Json is retrieved from API and then looping over the items I render the input elements. Here is the sample Json :
{
name: {
elementType: 'input',
label: 'Name',
elementConfig: {
type: 'text',
placeholder: 'Enter name'
},
value: '',
validation: {
required: true
},
valid: false,
touched: false
}
}
Here is how I render the form :
render() {
const formElementsArray = [];
for (const key in this.props.deviceConfig.sensorForm) {
formElementsArray.push({
id: key,
config: this.props.deviceConfig.sensorForm[key]
});
const itemPerRow = 4;
const rows = [
...Array(Math.ceil(props.formElementsArray.length / itemPerRow))
];
const formElementRows = rows.map((row, idx) =>
props.formElementsArray.slice(
idx * itemPerRow,
idx * itemPerRow + itemPerRow
)
);
const content = formElementRows.map((row, idx) => (
<div className='row' key={idx}>
{row.map((formElement) => (
<div className='col-md-3' key={formElement.id}>
<Input
key={formElement.id}
elementType={formElement.config.elementType}
elementConfig={formElement.config.elementConfig}
value={formElement.config.value}
invalid={!formElement.config.valid}
shouldValidate={formElement.config.validation}
touched={formElement.config.touched}
label={formElement.config.label}
handleChange={(event) => props.changed(event, formElement.id)}
/>
</div>
))}
</div>
...
}
I am storing the form state in redux and on every input change , I update the state. Now the problem is everytime I update the state, the entire form is re-rendered again... Is there any way to optimise it in such a way that only the form element which got updated is re-rendered ?
Edit :
I have used React.memo in Input.js as :
export default React.memo(input);
My stateful Component is Pure component.
The Parent is class component.
Edit 2 :
Here is how I create formElementArray :
const formElementsArray = [];
for (const key in this.props.deviceConfig.sensorForm) {
formElementsArray.push({
id: key,
config: this.props.deviceConfig.sensorForm[key]
});
You can make content as a separate component like this.
And remove formElementsArray prop from parent component.
export default function Content() {
const formElementRows = useForElementRows();
formElementRows.map((row, idx) => (
<Input
formId={formElement.id}
handleChange={props.changed}
/>
)
}
Inside Input.js
const handleInputChange = useCallback((event) => {
handleChange(event, formId);
}, [formId, handleChange]);
<input handleChange={handleInputChange} />
export default React.memo(Input)
So you can memoize handleChange effectively. And it will allow us to prevent other <Input /> 's unnecessary renders.
By doing this forElementRows change will not cause any rerender for other components.
You could try a container, as TianYu stated; you are passing a new reference as change handler and that causes not only the component to re create jsx but also causes virtual DOM compare to fail and React will re render all inputs.
You can create a container for Input that is a pure component:
const InputContainer = React.memo(function InputContainer({
id,
elementType,
elementConfig,
value,
invalid,
shouldValidate,
touched,
label,
changed,
}) {
//create handler only on mount or when changed or id changes
const handleChange = React.useCallback(
(event) => changed(event, id),
[changed, id]
);
return (
<Input
elementType={elementType}
elementConfig={elementConfig}
value={value}
invalid={invalid}
shouldValidate={shouldValidate}
touched={touched}
label={label}
handleChange={handleChange}
/>
);
});
Render your InputContainer components:
{row.map((formElement) => (
<div className="col-md-3" key={formElement.id}>
<InputContainer
key={formElement.id}
elementType={formElement.config.elementType}
elementConfig={formElement.config.elementConfig}
value={formElement.config.value}
invalid={!formElement.config.valid}
shouldValidate={formElement.config.validation}
touched={formElement.config.touched}
label={formElement.config.label}
//re rendering depends on the parent if it re creates
// changed or not
changed={props.changed}
/>
</div>
))}
You have to follow some steps to stop re-rendering. To do that we have to use useMemo() hook.
First Inside Input.jsx memoize this component like the following.
export default React.memo(Input);
Then inside Content.jsx, memoize the value of elementConfig, shouldValidate, handleChange props. Because values of these props are object type (non-primitive/reference type). That's why every time you are passing these props, they are not equal to the value previously passed to that prop even their value is the same (memory location different).
const elementConfig = useMemo(() => formElement.config.elementConfig, [formElement]);
const shouldValidate = useMemo(() => formElement.config.validation, [formElement]);
const handleChange = useCallback((event) => props.changed(event, formElement.id), [formElement]);
return <..>
<Input
elementConfig={elementConfig }
shouldValidate={elementConfig}
handleChange={handleChange}
/>
<../>
As per my knowledge, this should work. Let me know whether it helps or not. Thanks, brother.

Categories