I am using React.js (react hooks) and I have the following scenario:
I have a component parent (parent.jsx) that contains a submit button, the parent component has 4 children components (child1.jsx , child2.jsx, child3.jsx and child4.jsx).
Each child of these 4 children has its own fields, and when I press the submit button in the parent all the values of these fields will be submitted to an API call.
The idea is the following: I know that I can have a state in the parent for the whole fields in the children and with callbacks I can set the state, also I know that I can use useContext instead, but some of the fields are input and I am setting the state with onChnage .. this will cause the parent to be rendered as well as the 4 children .. and this will make the input laggy for the input fields.
I read that using useRef and useImperativeHandle could solve the problem so I can expose the state of the children to the parent and on submit I can send the values, also some said that I should be careful when it comes to useRef and useImperativeHandle . others recommend to use useContext with useMemo .
So as a react (hook) beginner, I am still not able to decide which approach is better or if there is a better one, but what I know is that I need to have the separation between the four children (maybe in the future I can reuse some in other components), so is there a recommended way to get what I want?
Note: I do not want to use "onBlue" instead "onChnage" I am concerned in solving the re-rendering problem more.
UPDATE
Example:(with a very naive way)
The parent:
import { useState } from 'react'
import Child1 from './child1'
import Child2 from './Child2'
import Child3 from './Child3'
import Child4 from './Child4'
function App() {
const [totalParentState, setTotalParentState] = useState({});
return (
<div className="App">
<Child1 setTotalState={setTotalParentState} totalState={totalParentState || {}}/>
<br/>
<Child2 setTotalState={setTotalParentState} totalState={totalParentState || {}}/>
<br/>
<Child3 setTotalState={setTotalParentState} totalState={totalParentState || {}}/>
<br/>
<Child4 setTotalState={setTotalParentState} totalState={totalParentState || {}}/>
<br/>
<button onClick={()=>{console.log(totalParentState || {})}}>Click me to console</button>
</div>
)
}
export default App
one of the children (the rest is the same but with different Object fields value):
import React, { useState } from 'react'
function Child4({setTotalState, totalState}) {
console.log("rerendeing child 4")
const [field1, setField1] = useState("");
const [field2, setField2] = useState("");
const [field3, setField3] = useState("");
const [field4, setField4] = useState("");
const [field5, setField5] = useState("");
const [field6, setField6] = useState("");
const [field7, setField7] = useState("");
const [field8, setField8] = useState("");
return (
<>
<input value={field1} onChange={(e) => {setField1(e.target.value); setTotalState({...totalState, child4f1:e.target.value})}}></input>
<input value={field2} onChange={(e) => {setField2(e.target.value); setTotalState({...totalState, child4f2:e.target.value})}}></input>
<input value={field3} onChange={(e) => {setField3(e.target.value); setTotalState({...totalState, child4f3:e.target.value})}}></input>
<input value={field4} onChange={(e) => {setField4(e.target.value); setTotalState({...totalState, child4f4:e.target.value})}}></input>
<br/>
<input value={field5} onChange={(e) => {setField5(e.target.value); setTotalState({...totalState, child4f5:e.target.value})}}></input>
<input value={field6} onChange={(e) => {setField6(e.target.value); setTotalState({...totalState, child4f6:e.target.value})}}></input>
<input value={field7} onChange={(e) => {setField7(e.target.value); setTotalState({...totalState, child4f7:e.target.value})}}></input>
<input value={field8} onChange={(e) => {setField8(e.target.value); setTotalState({...totalState, child4f8:e.target.value})}}></input>
</>
);
}
export default Child4;
When I change a value in one of the children I will get the console log for the child4 that it is re-rendered
UPDATE:
This is the other approach using the callback depending on Pooria Faramarzian suggestion:
parent:
import { useCallback, useEffect, useState } from 'react'
import Child1 from './child1'
import Child2 from './Child2'
import Child3 from './Child3'
import Child4 from './Child4'
function App() {
const [totalParentState, setTotalParentState] = useState({});
const [child1, setChild1] = useState({});
const [child2, setChild2] = useState({});
const [child3, setChild3] = useState({});
const [child4, setChild4] = useState({});
const child1Function = useCallback((value)=>{
setChild1({...child1, ...value});
},[child1])
const child2Function = useCallback((value)=>{
setChild2({...child2,...value})
},[child2])
const child3Function = useCallback((value)=>{
setChild3({...child3,...value})
},[child3])
const child4Function = useCallback((value)=>{
setChild4({...child4,...value})
},[child4]);
const setAllParent = () => {
setTotalParentState({...totalParentState,...child1, ...child2, ...child3, ...child4});
}
useEffect(()=> {
console.log(totalParentState)
},[totalParentState]);
return (
<div className="App">
<Child1 setTotalState={child1Function} />
<br/>
<Child2 setTotalState={child2Function} />
<br/>
<Child3 setTotalState={child3Function} />
<br/>
<Child4 setTotalState={child4Function} />
<br/>
<button onClick={()=>{(setAllParent())}}>Click me to console</button>
</div>
)
}
export default App
one of the children:
import React, { useState, memo } from 'react'
function Child4({setTotalState}) {
console.log("rerendeing child 4")
const [field1, setField1] = useState("");
const [field2, setField2] = useState("");
const [field3, setField3] = useState("");
const [field4, setField4] = useState("");
const [field5, setField5] = useState("");
const [field6, setField6] = useState("");
const [field7, setField7] = useState("");
const [field8, setField8] = useState("");
return (
<>
<input value={field1} onChange={(e) => {setField1(e.target.value); setTotalState({ child4f1:e.target.value})}}></input>
<input value={field2} onChange={(e) => {setField2(e.target.value); setTotalState({ child4f2:e.target.value})}}></input>
<input value={field3} onChange={(e) => {setField3(e.target.value); setTotalState({ child4f3:e.target.value})}}></input>
<input value={field4} onChange={(e) => {setField4(e.target.value); setTotalState({ child4f4:e.target.value})}}></input>
<br/>
<input value={field5} onChange={(e) => {setField5(e.target.value); setTotalState({ child4f5:e.target.value})}}></input>
<input value={field6} onChange={(e) => {setField6(e.target.value); setTotalState({ child4f6:e.target.value})}}></input>
<input value={field7} onChange={(e) => {setField7(e.target.value); setTotalState({ child4f7:e.target.value})}}></input>
<input value={field8} onChange={(e) => {setField8(e.target.value); setTotalState({ child4f8:e.target.value})}}></input>
</>
);
}
export default memo(Child4);
This will only cause the child component who its fields have been changed to be rendered ..
2
Answers
Depending on @Pooria Faramarzian suggestion I tired react-hook-form
and one of the children is like this:
and it does what I want
There are multiple ways to approach this scenario, and each approach has its own trade-offs. Here are some options you can consider:
Using state in the parent component and passing down callbacks as props:
This is a common pattern in React, and it involves passing down callback functions as props to the children components, which can then call those callbacks to update the state in the parent component. When the submit button is clicked, the parent component can gather all the values from its state and make the API call.
However, as you mentioned, this approach can cause the input fields to lag because the state updates trigger re-renders in the parent and all its children. To mitigate this issue, you can use the useCallback hook to memoize the callback functions and prevent unnecessary re-renders.
Using the useContext hook:
The useContext hook allows you to share the state between components without having to pass down props explicitly. You can create a context object in a separate file and use the useContext hook in the child components to access and update the shared state.
This approach can simplify your code and reduce the number of props you need to pass down, but it may not be as performant as the previous approach, especially if the shared state is complex or deeply nested.
Using useRef and useImperativeHandle:
This approach involves using the useRef hook to create a mutable object that can be accessed by the parent component, and the useImperativeHandle hook to expose the child components’ state to the parent component through a callback.
This approach can be useful when you need to expose a specific behavior or value of a child component to the parent component, but it may not be the best fit for this scenario because it can be more complex and error-prone.
Using a form library like Formik or React Hook Form:
Form libraries can provide a more structured and efficient way of handling form data in React. They can handle validation, submission, and other form-related tasks, and they can also reduce the amount of boilerplate code you need to write.
This approach can be useful if you have complex form logic or need to handle forms in multiple parts of your application. However, it may require a bit of a learning curve, and it may not be necessary for simpler forms.
In summary, the best approach depends on your specific use case and preferences. If performance is a concern, you can try the first approach and optimize it with the useCallback hook. If you prefer a more declarative approach, you can try a form library. And if you need to expose specific behavior or values of child components, you can try the useRef and useImperativeHandle approaches.