In React, props and state are everywhere - they allow us to pass information between components and manage the output of the component over time in response to various actions.
Using them separately is perfectly fine, but in some cases they are mixed:
const ParentComponent = () => {
const [value, setValue] = useState("Initial value");
const handleChange = (e) => {
setValue(e.target.value);
};
return <ChildComponent value={value} handleChange={handleChange} />;
};
const ChildComponent = ({ value, handleChange }) => (
<input value={value} onChange={handleChange} />
);
import React, { useState } from "react";
const ParentComponent = () => <ChildComponent initialValue="Initial value" />;
const ChildComponent = ({ initialValue }) => {
const [inputValue, setInputValue] = useState(initialValue);
const handleChange = (e) => {
setInputValue(e.target.value);
};
return <input value={inputValue} onChange={handleChange} />;
};
While there is nothing wrong with either example, the second may seem suspicious.
If you are not sure why, grab a cup of coffee and read on.
In general, props should be avoided in the initial state unless you only need them to initialize the internal state of the component and either props are never updated or the component should not react to their updates.
Let's look at the example above - we pass the initialValue prop, which is used to initialize the internal state of the ChildComponent and is never changed.
This is perfectly fine, but it is an exception rather than the general rule.
The real problem occurs when the initialValue in the ParentComponent can be changed:
import React, { useState, useEffect } from "react";
const ParentComponent = () => {
const [initialValue, setInitialValue] = useState("Initial value");
useEffect(() => {
setTimeout(() => {
setInitialValue("Changed value");
}, 1000);
}, []);
return <ChildComponent initialValue={initialValue} />;
};
const ChildComponent = ({ initialValue }) => {
const [inputValue, setInputValue] = useState(initialValue);
const handleChange = (e) => {
setInputValue(e.target.value);
};
return <input value={inputValue} onChange={handleChange} />;
};
In the above example, an initialValue is passed, which is changed from "Initial value" to "Changed value" after one second.
If you run an application and check what value is in the input element after one second, you will find that it is still "Initial value" even though it has changed in the parent component.
This happens because the useState hook initializes the state only once - when the component is rendered and is not able to capture further changes in the initialValue prop.
There is a second drawback - using props to generate the state often leads to duplication of the source of truth - you don't know where the real data is.
Now we know what the problem is with the above code, let's see a few ways to fix it.
In case if the parent component needs the value, for example to send it to the API, we can leave the state handling in the component and just pass the value and the update function as props:
const ParentComponent = () => {
const [value, setValue] = useState("Initial value");
useEffect(() => {
setTimeout(() => {
setValue("Changed value");
}, 1000);
}, []);
const handleChange = (e) => {
setValue(e.target.value);
};
return <ChildComponent value={value} handleChange={handleChange} />;
};
const ChildComponent = ({ value, handleChange }) => (
<input value={value} onChange={handleChange} />
);
This way, when the value is updated, the changes are immediately reflected in the input element.
If the parent component doesn't need the value, why would we leave it there to unnecessarily re-render the entire parent component?
Let's try moving the state and update function to the child component:
import React, { useState, useEffect } from "react";
const ParentComponent = () => {
const [initialValue, setInitialValue] = useState("Initial value");
useEffect(() => {
setTimeout(() => {
setInitialValue("Changed value");
}, 1000);
}, []);
return <ChildComponent initialValue={initialValue} />;
};
const ChildComponent = ({ initialValue }) => {
const [inputValue, setInputValue] = useState("");
useEffect(() => {
if (initialValue) {
setInputValue(initialValue);
}
}, [initialValue]);
const handleChange = (e) => {
setInputValue(e.target.value);
};
return <input value={inputValue} onChange={handleChange} />;
};
Note that we need to use the useEffect hook to capture the change in initialValue and update the state accordingly.
We can also use the initialValue as a key to create a new instance of the ChildComponent each time it changes to reset the state:
import React, { useState, useEffect } from "react";
const ParentComponent = () => {
const [initialValue, setInitialValue] = useState("Initial value");
useEffect(() => {
setTimeout(() => {
setInitialValue("Changed value");
}, 1000);
}, []);
return <ChildComponent key={initialValue} initialValue={initialValue} />;
};
const ChildComponent = ({ initialValue }) => {
const [inputValue, setInputValue] = useState(initialValue);
const handleChange = (e) => {
setInputValue(e.target.value);
};
return <input value={inputValue} onChange={handleChange} />;
};
While this is fine for small components like we have, keep in mind that rebuilding components from scratch (which is done when we change the key prop) can get expensive if the components are large.
In this article, we learned why it is better not to use props as arguments to the useState hook.
In some cases it is possible, but only if you are sure that you do not want the component to react to the changes in props, like in our example with the initial state - if it can never be changed, it is perfectly fine to initialize the state with it.
There are a few ways to fix an issue if you run into it:
I suggest you stick to one of the first two solutions depending on your requirements, as the third solution may have some (rather minor) impact on the performance of your application.