The React documentation describes how to conveniently and effectively manage state changes in your components by passing handlers through props. This helps avoid overengineering and promotes component reusability. However, this often leads to increased complexity in the root component.
Consider the provided diagram, showing a simple component hierarchy with Root, Child A, Child B, and Child C. Child C contains logic that is functionally independent from the other tasks of the Root component. However, to function correctly, Child C requires a prop that is modified by Child B.
Child A and Child B use handlers passed through props. Child B changes a prop (e.g., counter), passes it to Child A, which in turn passes it to the Root component. This approach is by the book and is correct.
Now, Child C uses useEffect to respond to changes in the prop (e.g., counter) and to handle the necessary logic to continue functioning correctly.
Problem Statement and Discussion
It’s understandable that this use of useEffect might raise concerns, suggesting that the states within Child C should be moved to the Root component to avoid using useEffect. This is generally correct as it can eliminate the side-effect. According to React documentation, this can be managed properly.
Advantages of the Current Structure
- Decoupling and Complexity:
- Keeping the logic within Child C ensures that it remains independent
and maintainable. - The Root component is not overloaded with additional logic, which keeps it simpler and more maintainable.
- Reusability:
- Child C remains reusable because it is not tightly coupled with the Root component.
- Strong coupling would mean Child C now requires multiple props (e.g., prop.min, prop.max, and prop.counter), complicating its use and reducing reusability.
Code Example
// Root Component
function Root() {
const [counter, setCounter] = useState(0);
const [globalSetting, setGlobalSetting] = useState(true);
const incrementCounter = () => setCounter(counter + 1);
const toggleGlobalSetting = () => setGlobalSetting(prev => !prev);
return (
<div>
<ChildA counter={counter} incrementCounter={incrementCounter} />
<ChildC counter={counter} globalSetting={globalSetting} />
</div>
);
}
// ChildA Component
function ChildA({ counter, incrementCounter }) {
return (
<div>
<ChildB incrementCounter={incrementCounter} />
</div>
);
}
// ChildB Component
function ChildB({ incrementCounter }) {
return (
<button onClick={incrementCounter}>Increment</button>
);
}
// ChildC Component
function ChildC({ counter, globalSetting }) {
const [localState, setLocalState] = useState(null);
const [anotherLocalState, setAnotherLocalState] = useState(false);
useEffect(() => {
// Logic to handle change in counter
console.log("Counter changed:", counter);
// Updating local state based on counter change
setLocalState(counter * 2);
}, [counter]);
useEffect(() => {
// Logic to handle change in globalSetting
console.log("Global setting changed:", globalSetting);
// Updating another local state based on globalSetting change
setAnotherLocalState(globalSetting);
}, [globalSetting]);
return (
<div>
<div>Counter: {counter}</div>
<div>Local State: {localState}</div>
<div>Another Local State: {anotherLocalState ? "True" : "False"}</div>
</div>
);
}
Conclusion
Deciding whether to keep the logic in Child C or move it to the Root component requires careful consideration. The reusability and maintainability of the components should be prioritized. Using useEffect in Child C keeps the component independent and reusable, while not unnecessarily complicating the Root component.
I thought it would be a good idea to have a discussion about the “it depends”-factor of this.