onChange setState is rerendering all components? - reactjs

I have a webpage with multiple forms. Here's a bare minimum example of the structure:
export default function Example() {
const [previousFormFetchedFromWeb, setPreviousFormFetchedFromWeb] = useState(
{}
);
const [formA, setFormA] = useState({});
const [formB, setFormB] = useState({});
const router = useRouter();
useEffect(() => {
fetchFormFromWeb(router.query.id).then((previousForm) => {
console.log("fetched info from web");
setPreviousFormFetchedFromWeb(previousForm);
setFormA(previousForm.formA);
setFormB(previousForm.formB);
});
},[router.isReady]);
return (
<>
<FormA form={formA} />
<FormB form={formB} />
</>
);
}
function FormA({ form }) {
return (
<input
type="text"
name="field1"
id="field1"
value={formA.field1}
onChange={(e) => {
setFormA(e.target.value);
}}
/>
);
}
function FormB({ form }) {
return (
<input
type="text"
name="field2"
id="field2"
value={formB.field2}
onChange={(e) => {
setFormB(e.target.value);
}}
/>
);
}
I think this should be the encapsulated logic of my form page. The problem is that when the onChange event is called for a field of any form, all forms get re-rendered. I assumed that setState should re-render only the components with the affected dependency change. Am I missing something?

Any state change in Example component will trigger re render to its child components (FormA, FormB). If you want to avoid that. wrap FormA and FormB in React.memo. That way you can prevent unwanted re-renders
export default function Example() {
const [previousFormFetchedFromWeb, setPreviousFormFetchedFromWeb] = useState(
{}
);
const [formA, setFormA] = useState({});
const [formB, setFormB] = useState({});
const router = useRouter();
useEffect(() => {
fetchFormFromWeb(router.query.id).then((previousForm) => {
console.log("fetched info from web");
setPreviousFormFetchedFromWeb(previousForm);
setFormA(previousForm.formA);
setFormB(previousForm.formB);
});
},[router.isReady]);
return (
<>
<MemFormA form={formA} />
<MemFormB form={formB} />
</>
);
}
const MemFormA = React.memo(function FormA({ form }) {
return (
<input
type="text"
name="field1"
id="field1"
value={formA.field1}
onChange={(e) => {
setFormA(e.target.value);
}}
/>
);
})
const MemFormB = React.memo(function FormB({ form }) {
return (
<input
type="text"
name="field2"
id="field2"
value={formB.field2}
onChange={(e) => {
setFormB(e.target.value);
}}
/>
);
})

Anytime state of <Example /> is updated, <Example /> re-renders, which in turn also re-renders <FormA /> and <FormB />. This is expected.
You should look into using React.memo() for FormA and FormB if you want them to only re-render when the props passed to them is changed.

Related

Change variable value only when a state changes in React

I have the code below.
How can I reset the stocksHelper, instatiating again when the component render on stocks's useState change?
I need this class to instantiate again to reset the variables inside the class instance, because when the stocks change a calculation needs to be done to render the stocks again. And if I get the instance of the last render with the old values this calculation will bug my entire aplication
export default function Heatmap() {
const [stocks, setStocks] = useState<IStock[]>([]);
const stocksHelper: StocksHelper = new StocksHelper(stocks);
return (
<main className={styles.main}>
<RegisterForm stocks={stocks} setStocks={setStocks} />
</main>
);
}
RegisterForm component below:
export default function RegisterForm(props: Props) {
const { stocks, setStocks } = props;
const [name, setName] = useState<string>('');
const [value, setValue] = useState<number>(0);
const [volume, setVolume] = useState<number>(0);
function storeStock(): void {
axios.post('any url', {
name: name,
value: value,
volume: volume
})
.then((res) => {
setStocks([...stocks, res.data]);
})
.catch((res) => console.log(res));
}
return (
<form className={styles.form} onSubmit={() => storeStock()}>
<fieldset>
<legend className={styles.title}>Cadastro</legend>
<input type="text" onChange={e => setName(e.target.value)} placeholder="Nome" />
<input type="number" onChange={e => setValue(parseFloat(e.target.value))} placeholder="Porcentagem" />
<input type="number" onChange={e => setVolume(parseInt(e.target.value))} placeholder="Volume" />
<button type='submit'>Cadastrar</button>
</fieldset>
</form>
);
}
#AmitMaraj's answer is perfectly fine but for a shorter and more concise method you should use useMemo:
const stocksHelper = useMemo(() => new StocksHelper(stocks), [stocks]);
Now a new StocksHelper will only be created when stocks changes.
Pass a “create” function and an array of dependencies. useMemo will only recompute the memoized value when one of the dependencies has changed. This optimization helps to avoid expensive calculations on every render.
Link to documentation
If I'm understanding correctly, you might be able to achieve this with useEffect! See below for an example:
export default function Heatmap() {
const [stocks, setStocks] = useState<IStock[]>([]);
const [stocksHelper, setStockHelper] = useState<StocksHelper>(new StocksHelper(stocks));
useEffect(() => {
setStockHelper(new StocksHelper(stocks))
}, [stocks])
return (
<main className={styles.main}>
<RegisterForm stocks={stocks} setStocks={setStocks} />
</main>
);
}

How to call parent function from children in composition?

I want to create an Input component to be used to compose new form elements.
const Input = ({ value, children }) => {
const [currentValue, setCurrentValue] = useState();
return <div className='input'>
{children}
</div>
};
And my Text component would be:
const Text = (props) => {
return <Input {...props}>
<input
type='text'
value={/*how to bind to currentValue of Input*/}
onChange={/*how to call Input's setCurrentValue here*/}
/>
</Input>
}
I need to store currentValue state in parent, because I need to manage it for many different inputs.
Also I'm stuck at how to call parent's setCurrentValue on child's onChange method.
Any help is appreciated.
Update:
CodeSandbox
Update 2:
Real code from my repository
Solutions:
Context API
Pass props to children
Use children as funciton & pass relevant
Send that reference somehow using any method you see fit.
My preference: Composition with function
const Input = ({ value, children }) => {
const [currentValue, setCurrentValue] = useState();
const handlChange = (e) => {
setCurrentValue(e.target.value);
};
return <div className='input'>
{children(handlChange)}
</div>
};
const Text = (props) => {
return <Input {...props}>
{ (handleChange) => (
<input
type='text'
onChange = ( handleChange }
/>
) }
</Input>
}
Explanations:
How to pass props to {this.props.children}
https://victorofoegbu.com/notes/pass-props-to-react-children-faq
Please try like this.
// pass props to children.
const Input = ({ value, children }) => {
const [currentValue, setCurrentValue] = useState();
return <div className='input'>
{React.cloneElement(child, {onChange: setCurrentValue, value: currentValue}))}
</div>
};
// using props in childern.
const Text = (props) => {
return <Input {...props}>
{
({onChange, value})=> (
<input
type='text'
value={value}
onChange={(e)=>onChange(e.target.value)}
/>
)
}
</Input>
}

Losing focus when changing state inside onFocus event when using custom input

How to keep input focus when changing state inside onFocus event
seems like it's not happening with simple input
only when using customInput
const CustomInput = ({ onFocus, state }) => {
return (
<div>
<input onFocus={onFocus} />
</div>
);
};
const TestInput = () => {
const [state, setState] = React.useState(false);
return (
<FilledInput
inputComponent={(props) => {
return (
<CustomInput onFocus={props.onFocus} state={state} />
);
}}
onFocus={() => {
setState(true);
}}
/>
);
};

how to use form in React as a component

I tried to put a form in a separate reusable component but when used that way I can't type anything into the input. I observed, that after entering one letter (it does not appear in the input box) it seems that React rerender the whole component and the name is updated with the inserted letter.
in the version 2 the same code works correctly.
// the part same for the both versions
const [userdata, setUser] = useState({});
const { name } = userdata
const handleChange = key => event => {
setUser({
...userdata,
[ key ]: event.target.value
});
};
const submitEdit = event => {
event.preventDefault();
handleChange();
};
// VERSION 1. doesn't work
const FormEdit = () => (
<form>
<div className="form-group">
<input onChange={handleChange("name")} type="text"/>
</div>
<button onClick={submitEdit}> Submit </button>
</form>
)
return (
<Layout>
<div>
{name} //<-it shows only one letter
<FormEdit />
</div>
</Layout>
);
// VERSION 2 -> works properly
return (
<Layout>
<div>
{name} //<-the updated name is shown immediately
<form>
<div className="form-group">
<input onChange={handleChange("name")} type="text"/>
</div>
<button onClick={submitEdit}> Submit </button>
</form>
</div>
</Layout>
);
};
export default User;
The issue is directly related to declaring the FormEdit component within the other component. Here's why:
In a functional component, everything declared inside gets destroyed and re-created each render. It's no different than a normal function call. This is what makes React's hooks so special. They keep track of values in between renders and make sure they are re-created with the correct values.
You're declaring the FormEdit component inside a function, which means not only is it re-declared every render, but as a side-effect it also un-mounts and remounts each render as well.
This has a few different effects:
The component's input loses focus every render.
It's impossible for it to maintain its own state.
It's not very performant.
Below is a working example to demonstrate.
const {useState, useEffect} = React;
const Example = () => {
// the part same for the both versions
const [userdata, setUser] = useState({});
const { name } = userdata
const handleChange = (key) => (event) => {
setUser({
...userdata,
[ key ]: event.target.value
});
};
const submitEdit = (event) => {
event.preventDefault();
handleChange();
};
const FormEdit = () => {
useEffect(() => {
console.log('mount');
return () => console.log('unmount');
}, []);
return (
<form>
<div>
<input onChange={handleChange("name")} type="text"/>
</div>
<button onClick={submitEdit}> Submit </button>
</form>
)
}
return (
<div>
{name}
<FormEdit />
</div>
);
}
ReactDOM.render(<Example />, document.getElementById('root'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>
<div id="root"></div>
As for why you only see the first character; You are not giving the input a value, only an onChange. If the component does not unmount, this just makes it an "uncontrolled" component. The input still gets it's value updated, you just can't programatically control it. But, since it is unmounting and re-mounting every render, it loses its last value every time the user types.
Making it a controlled input would fix this:
const {useState, useEffect} = React;
const Example = () => {
// the part same for the both versions
const [userdata, setUser] = useState({});
const { name } = userdata
const handleChange = (key) => (event) => {
setUser({
...userdata,
[ key ]: event.target.value
});
};
const submitEdit = (event) => {
event.preventDefault();
handleChange();
};
const FormEdit = () => {
useEffect(() => {
console.log('mount');
return () => console.log('unmount');
}, []);
return (
<form>
<div>
<input value={name} onChange={handleChange("name")} type="text"/>
// ^ Add this
</div>
<button onClick={submitEdit}> Submit </button>
</form>
)
}
return (
<div>
{name}
<FormEdit />
</div>
);
}
ReactDOM.render(<Example />, document.getElementById('root'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>
<div id="root"></div>
This is a little better, but still not ideal. Now it keeps the value each update, but it still loses focus. Not a very good user experience.
This final solution is to never declare a component within another component.
const {useState, useEffect} = React;
const FormEdit = (props) => {
useEffect(() => {
console.log('mount');
return () => console.log('unmount');
}, []);
return (
<form>
<div>
<input value={props.name} onChange={props.handleChange("name")} type="text"/>
</div>
<button onClick={props.submitEdit}> Submit </button>
</form>
)
}
const Example = () => {
// the part same for the both versions
const [userdata, setUser] = useState({});
const { name } = userdata
const handleChange = (key) => (event) => {
setUser({
...userdata,
[ key ]: event.target.value
});
};
const submitEdit = (event) => {
event.preventDefault();
handleChange();
};
return (
<div>
{name}
<FormEdit name={name} handleChange={handleChange} submitEdit={submitEdit} />
</div>
);
}
ReactDOM.render(<Example />, document.getElementById('root'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>
<div id="root"></div>
Now it only mounts once, keeps focus, and updates as expected.
You would have to pass your form handlers to the child component as props so that the lifted state can be manipulated from the child.
// Parent Component
...
const [userdata, setUser] = useState({});
const { name } = userdata
const handleChange = key => event => {
...
};
const submitEdit = event => {
...
};
return (
<Layout>
<div>
{name}
<FormEdit handleChange={handleChange} submitEdit={submitEdit}/>
</div>
</Layout>
);
and then in the child:
// Child Component
const FormEdit = (props) => (
<form>
<div className="form-group">
<input onChange={props.handleChange("name")} type="text"/>
</div>
<button onClick={props.submitEdit}> Submit </button>
</form>
)
Your FormEdit component which is inside the App component is causing the entire App component to re-render when the state gets updated onChange and hence you can only enter only one character at a time. It is generally not a great idea to declare a component within a component. Refer this link for more info. All you have to do is pull the FormEdit component out of the App component in its own separate function and pass the change handlers as props to the FormEdit component. Have a look at the working code below.
import React, { useState } from 'react';
const FormEdit = ({ handleChange, submitEdit, name }) => {
return (
<form>
<div className='form-group'>
<input onChange={handleChange('name')} type='text' value={name || ''} />
</div>
<button onClick={submitEdit} type='submit'>
Submit
</button>
</form>
);
};
export default function App() {
const [userdata, setUser] = useState();
const { name } = userdata || {};
const handleChange = key => event => {
setUser(prevState => {
return { ...prevState, [key]: event.target.value };
});
event.persist();
event.preventDefault();
};
const submitEdit = event => {
event.preventDefault();
handleChange();
};
return (
<div>
<div>
{name || ''}
<FormEdit
handleChange={handleChange}
submitEdit={submitEdit}
name={name}
/>
</div>
</div>
);
}

Multiple Inputs

I want to set a Error Message under my Input Fields, if the value isn't valid. The message will always be the same. But the message is shown on every Input field, if the value isn't valid. That's logic if you see the code. But how can I make it work?
And yes, I have to use the error state in the Parent component because I have lifted the state up. Because of style dependencies of the error prop. I'm using styled-components for that.
Parent component:
const List = (props) => {
const [error, setError] = useState('');
function handleChange(event) {
const currentValue = parseInt(event.target.value, 10);
if (currentValue > 10) {
setError('Error');
} else setError(null);
}
return (
{items.map(item => (
<MyInput
value={item.value}
key={item.input}
error={error}
handleChange={e => handleChange(e)}
/>
))}
);
};
export default List;
The child Component:
const MyInput = (props) => {
return (
<>
<input
defaultValue={props.value}
type="text"
placeholder={props.value}
onChange={e => props.handleChange(e)}
/>
{props.error ? {props.error} : null}
</>
);
};
export default MyInput;
Is it possible to use the state in the parent component?
I hope you can help me :)

Resources