I have a simple for with some fields in it, the fields being child components to that form. Each field validates its own value, and if it changes it should report back to the parent, which causes the field to re-render and lose focus. I want a behavior in which the child components do not update. Here's my code:
Parent (form):
function Form() {
const [validFields, setValidFields] = useState({});
const validateField = (field, isValid) => {
setValidFields(prevValidFields => ({ ...prevValidFields, [field]: isValid }))
}
const handleSubmit = (event) => {
event.preventDefault();
//will do something if all fields are valid
return false;
}
return (
<div>
<Title />
<StyledForm onSubmit={handleSubmit}>
<InputField name="fooField" reportState={validateField} isValidCondition={fooRegex} />
<Button type="submit" content="Enviar" maxWidth="none" />
</StyledForm>
</div>
);
}
export default Form;
Child (field):
function InputField(props) {
const [isValid, setValid] = useState(true);
const [content, setContent] = useState("");
const StyledInput = isValid ? Input : ErrorInput;
const validate = (event) => {
setContent(event.target.value);
setValid(stringValidator.validateField(event.target.value, props.isValidCondition))
props.reportState(props.name, isValid);
}
return (
<Field>
<Label htmlFor={props.name}>{props.name + ":"}</Label>
<StyledInput
key={"form-input-field"}
value={content}
name={props.name}
onChange={validate}>
</StyledInput>
</Field>
);
}
export default InputField;
By setting a key for my child element I was able to prevent it to lose focus when content changed. I guess I want to implement the shouldComponentUpdate as stated in React documentation, and I tried to implement it by doing the following:
Attempt 1: surround child with React.memo
const InputField = React.memo((props) {
//didn't change component content
})
export { InputField };
Attempt 2: intanciate child with useMemo on parent
const fooField = useMemo(<InputField name="fooField" reportState={validateField} isValidCondition={fooRegex} />, [fooRegex]);
return (
<div>
<Title />
<StyledForm onSubmit={handleSubmit}>
{fooField}
<Button type="submit" content="Enviar" maxWidth="none" />
</StyledForm>
</div>
);
Both didn't work. How can I make it so that when the child component isValid state changes, it doesn't re-render?
The problem is not that the component is re-rendering, it is that the component is unmounting given by this line:
const StyledInput = isValid ? Input : ErrorInput;
When react unmounts a component, react-dom will destroy the subtree for that component which is why the input is losing focus.
The correct fix is to always render the same component. What that means to you is based on how your code is structured, but I would hazard a guess that the code would end up looking a bit more like this:
function InputField(props) {
const [isValid, setValid] = useState(true);
const [content, setContent] = useState("");
const validate = (event) => {
setContent(event.target.value);
setValid(stringValidator.validateField(event.target.value, props.isValidCondition))
props.reportState(props.name, isValid);
}
return (
<Field>
<Label htmlFor={props.name}>{props.name + ":"}</Label>
<Input
valid={isValid} <-- always render an Input, and do the work of displaying error messages/styling based on the `valid` prop passed to it
value={content}
name={props.name}
onChange={validate}>
</Input>
</Field>
);
}
The canonical solution to avoiding rerendering with function components is React.useMemo:
const InputField = React.memo(function (props) {
// as above
})
However, because validateField is one of the props passed to the child component, you need to make sure it doesn't change between parent renders. Use useCallback to do that:
const validateField = useCallback((field, isValid) => {
setValidFields(prevValidFields => ({ ...prevValidFields, [field]: isValid }))
}, []);
Your useMemo solution should also work, but you need to wrap the computation in a function (see the documentation):
const fooField = useMemo(() => <InputField name="fooField" reportState={validateField} isValidCondition={fooRegex} />, [fooRegex]);
Related
Using hooks.
The parent component passes a value to the child component, which the child component displays. But I'd like that component editable and once the user clicks save, its new value should be passed back to parent component and is then used to update.
parent component:
const [value, setValue] = useState("");
// value is set by some function
<Child
value={value}
/>
child component:
<h2 contentEditable=true >props.value</h2>
// somehow save the value, even to a different variable, and pass it back to parent component to setValue
I have tried setting in my child component file something like
const childValue=props.value
or
const [childValue, setChildValue] = useState(props.value)
and I tried console.log on those values but they're all empty, and if I passed them to h2 in the child component, nothing displays.
--- EDIT
I have tried passing the function to set value to parent but I'm struggling getting the changed value.
For child component I'd like to save either onChange or on clicking a button, but I'm missing a way to capture the new value.
parent component has a saveValue(newValue) function which I pass to child
in child component
const {value, saveValue} = props;
return
<h2
onChange={() => saveValue(e.target.value)}
contentEditable=true> {value} </h2>
I have saveValue in parent component and tried changing the function to print out the argument in the console but nothing gets logged.
I also tried saving the changes with a button, I have no method of capturing the actual changes made to h2 though, as my code looks something like this:
const {value, setValue} = props;
<h2 contentEditable=true>{value}</h2>
<button onClick={()=>setValue(value)}>Save</button>
This just sets the value to the old value and not the edited one
Pass setValue to the Child component as a prop, and call it inside. For example:
const ChildComponent = ({value, setValue}) => {
return (
<>
<button onClick={() => setValue('my new value')}>
change value
</button>
<span>{value}</span>
</>
)
}
const ParentComponent = () => {
const [value, setValue] = useState("");
return <ChildComponent value={value} setValue={setValue}/>
}
You can approach this problem in 2 ways. Both the approach are basically the same thing, as you are ultimately passing a function to handle the state change.
Approach 1: Create a handler function (say handleValueChange) to manage the state change using setValue in the parent component. Then you can pass this handler function to the child component.
Parent component:
const Parent = () => {
const [value, setValue] = useState("");
function handleChange() {
// Some logic to change the "value" state
setValue("A new value");
}
return <Child value={value} handlerFunction={handleChange} />
}
Child component:
const Child = (props) => {
const { value, handlerFunction } = props;
// Utilize the props as per your needs
return (
<>
<h2 contentEditable>Value: {value}</h2>
<button onClick={handlerFunction}>Change state</button>
</>
);
};
Approach 2: Pass on the setValue function to the child component as props.
Parent component:
const Parent = () => {
const [value, setValue] = useState("");
return <Child value={value} setValue={setValue} />
}
Child component:
const Child = (props) => {
const { value, setValue } = props;
// Utilize the props to change the state, as per your needs
function handleChange() {
setValue("A new value");
}
return (
<>
<h2 contentEditable>Value: {value}</h2>
<button onClick={handleChange}>Change state</button>
</>
);
};
I'm using React JS and have faced a problem.
I have a component on the page which has some inputs. When user clicks on any input a new block should be created below and the same input has to be focused at the same time.
Everything worked until I've created a show logic:
const readyBlock = isTouched ? <ViewModule textInput={textInput}/> : null;
After that I get ×
TypeError: Cannot read property 'focus' of null
Below is my Main component where everything on the page happens.
const Sales = () => {
const [isTouched, setIsTouched] = useState(false);
const textInput = useRef(null);
function handleInput() {
setIsTouched(true);
textInput.current.focus();
}
const readyBlock = isTouched ? <ViewModule textInput={textInput}/> : null;
return (
<main className="sales-page">
<div className="main__title">
<h2 className="main__heading">Bonuses</h2>
</div>
<div className="content-container">
<UploadForm>
<FileUploadInput
handleChange={handleInput}
placeholder="Header"/>
<FileUploadTextArea placeholder="Descr"/>
</UploadForm>
</div>
<div className="ready-container ">
{readyBlock}
</div>
</main>
)
}
const ViewModule = ({textInput}) => {
return (
<UploadForm classNames="textarea-written">
<FileUploadInput
ref={textInput}
placeholder="Заголовок"/>
<FileUploadTextArea placeholder="Descr"/>
<div className="btn-container">
<Btn classNames="cancel-btn">Cancel</Btn>
<Btn>Save</Btn>
</div>
</UploadForm>
)
}
Below is an input component:
const FileUploadInput = React.forwardRef((props, ref) => {
return (
<div className="text-input-wrapper">
<input
ref={ref}
type="text"
id="file-text-input"
name="file__upload-title"
placeholder={props.placeholder}
onClick={props.handleChange} />
</div>
)
});
I assume you want to focus the ViewModule component when its added.
The problem is that the Ref textInput is not assigned to any component before the ViewModule is added to the DOM tree. You would have to first add the ViewModule to the DOM tree on state change and then later in a useEffect hook you will find the textInput Ref properly assigned.
function handleInput() {
setIsTouched(true);
}
useEffect(() => {
if (textInput.current === null) return
if (isTouched) textInput.current.focus();
}, [isTouched])
Also you should pass the textInput Ref to ViewModule using React.forwardRef as you did for FileUploadInput.
const ViewModule = React.forwardRef((ref) => {...});
And use it like this.
const readyBlock = isTouched ? <ViewModule ref={textInput}/> : null;
Scenario
I declared a react component that renders a simple html input tag.
const MyComponent = (props) => (
<input
defaultValue="test"
onChange={(e) => {
props.setTitle(e.target.value);
}}
/>
);
Then I declared a function that takes a setState as a parameter and returns that component with the setState inside the input's onChange.
const getComponent = (setTitle) => (props) => (
<input
defaultValue="test"
onChange={(e) => {
setTitle(e.target.value);
}}
/>
);
Then I called my function to get the component and to render it:
const Root = () => {
const [title, setTitle] = React.useState('');
const Component = getComponent(setTitle);
return (
<div>
<div>{title}</div>
<Component />{' '}
</div>
);
};
Expected:
The input element behaves normally, and changes its value
Reality:
The input loses focus after each character typed, and won't retain its value.
Here is a simple example of the error:
CodeSandbox
The reason this is happening is that when your code comes to this line:
const Component = getComponent(setTitle);
This generates new function (i.e. new instance) that is not rendered again, but mounted again. That is the reason you get unfocused from field.
There is no way you will make this work in this way, it's just not meant to be working like this. When you do it once when you are exporting it, than its ok, but every time === new instance.
If this is just an experiment that you are trying, than ok. But there is no reason not to pass setState as prop to that component.
I found a solution that let me use the function and keep the component from mounting each time.
If you put the function call inside the jsx, the component won't remount each render.
const Root = () => {
const [title, setTitle] = React.useState('');
const Component = ;
const someprops = {};
return (
<div>
<div>{title}</div>
{getComponent(setTitle)(someprops)}
</div>
);
};
I have a root component as:
const EnterMobileNumberPage: React.FC = () => {
return (
<div
className="Page"
id="enterMobileNumberPage"
>
<CardView>
<p
className="TitleLabel"
>
Please enter your mobile number
</p>
<input
className="PlainInput"
type="text"
maxLength={10}
onChange={inputAction}
/>
<FilledButton
title="Next"
action={buttonAction}
invalid
/>
</CardView>
</div>
);
}
Where CardView and FilledButton are my custom components. FilledButton has logic shown below:
type FilledButtonProps = {
title: string,
bgcolor?: string,
color?: string,
invalid?: boolean,
action?: ()=>void
}
const FilledButton: React.FC<FilledButtonProps> = (props) => {
const [, updateState] = React.useState();
const forceUpdate = React.useCallback(() => updateState({}), []);
let backgroundColor: string | undefined
if(props.bgcolor){
backgroundColor = props.bgcolor
}
if(props.invalid === true){
backgroundColor = "#bcbcbc"
}
const overrideStyle: CSS.Properties = {
backgroundColor: backgroundColor,
color: props.color
}
return (
<a
className="FilledButton"
onClick={props.action}
>
<div style={overrideStyle}>
{props.title}
</div>
</a>
);
}
Here, I want to listen to text change event in input element. What should I write so that inputAction has a way to update FilledButton?
For example, I may want to change FilledButton's invalid to false when input element has 10 digits number.
(I didn't introduce Redux, since I'm quite a beginner)
so if you want to update the props receibe by <FilledButton />, you only need to store a state (call it action, maybe) when your inputAction onChange function is trigger, that way you'll update that state and that state is been pass to you children component:
import React, { useState } from 'react';
const EnterMobileNumberPage: React.FC = () => {
const [action, setAction] = React.useState('');
const handleChange = e => {
if (e && e.target && e.target.value) {
setAction(e.target.value);
}
};
return (
<div
className="Page"
id="enterMobileNumberPage"
>
<CardView>
<p className="TitleLabel" >
Please enter your mobile number
</p>
<input
className="PlainInput"
type="text"
maxLength={10}
onChange={handleChange}
/>
<FilledButton
title="Next"
action={buttonAction}
invalid={action.length === 10}
/>
</CardView>
</div>
);
}
Then, you'll have an action estate, that you could use to block your <FilledButton />and also to use it as the <input /> value, Hope this helps.
Since you want to update sibling component, the only way you have is to re-render parent component and pass updated prop to sibling component so it will also update.
const EnterMobileNumberPage: React.FC = () => {
const [mobileVal, inputAction] = React.useState('');
return (
<div>
<input
className="PlainInput"
type="text"
maxLength={10}
onChange={inputAction} // Updating parent component state on change
/>
<FilledButton
title="Next"
action={buttonAction}
invalid
mobileLength={mobileVal.length} // New prop for filled button
/>
</div>
);
}
The Problem
I have a form to send data through a api rest in React, the render and the writing on the form is very slow when I have about 80 text fields.
I'm using functional components with hooks to handle the input texts and Material-UI as UI framework.
In a first try, I had a currying function to handle the values:
setValue = (setter) => (e) => { setter(e.target.value) }
But the render process was really slow (because I was creating a function in every render), So I send the setter function as a prop, then it improves a little but not enough.
Actually the input response when I write a key in any input, it's about 500 ms.
What can I do to get a better performance?
The code was simplified for understanding purposes.
Sample code below:
const [input1, setInput1] = useState('')
const [input2, setInput2] = useState('')
const [input3, setInput3] = useState('')
.
.
.
const [input80, setInput80] = useState('')
// render the Inputs
<Input value={input1} setter={setInput1} text="Some label text" />
<Input value={input2} setter={setInput2} text="Some label text" />
<Input value={input3} setter={setInput3} text="Some label text" />
.
.
.
<Input value={input80} setter={setInput80} text="Some label text" />
My Input components:
const Input = ({
value, setter, text, type = 'text'
}) => {
const handleChange = (e) => {
const { value: inputValue } = e.target
setter(inputValue)
}
return (
<Grid>
<TextField
fullWidth
type={type}
label={text}
value={value}
onChange={handleChange}
multiline={multiline}
/>
</Grid>
)
}
All input values are must be in a component because I'm need to send them to a server with axios.
It looks like the Material-UI Input component is a bit heavy.
I have a sample codesandbox here where I initialised around 1000 inputs. Initially it lagged and crashed.
To begin with I added a memo to the Input component. This memoizes all the Input components, triggering a new render only if one of its props has changed.
For a start just add a memo to your input component.
import React, { memo } from 'react';
const Input = memo(({
value, setter, text, type = 'text'
}) => {
const handleChange = (e) => {
const { value: inputValue } = e.target
setter(inputValue)
}
return (
<Grid>
<TextField
fullWidth
type={type}
label={text}
value={value}
onChange={handleChange}
multiline={multiline}
/>
</Grid>
)
})
Note: If you have a custom setter like in your first case setValue = (setter) => (e) => { setter(e.target.value) }, you can wrap that in a useCallback to prevent multiple functions to be created for every render.