Reputation: 800
I am using Material-UI within my ReactJS app to create a table that, when clicked, expands to show more detailed info (a new row just beneath the clicked row). As example, here is a minimal toy example:
https://codesandbox.io/s/material-collapse-table-forked-t6thz
The code relevant to the problem is:
<Collapse
in={open}
timeout="auto"
TransitionProps={{
mountOnEnter: true,
unmountOnExit: true,
}}
mountOnEnter
unmountOnExit
>
<div>
{/* actual function calls here; produces JSX output */}
{console.log("This should not execute before expanding!")}
Hello
</div>
</Collapse>;
Do note that the console.log()
statement is just a simple replacement for my actualy functionality, which involves some API calls that are made when a row is clicked, and the corresponding info is displayed. So instead of console.log()
I would actually call some other function.
I find that the console.log()
statement executed on initial page render itself, even though in=false
initially. How can I prevent this? Such that the function calls take place only when the Collapse is expanded. I initially thought this would be automatically handled by using mountOnEnter
and unmountOnExit
, but that does not seem to be the case. Any help would be appreciated, that could fix this problem in the sample example above.
I am working on an existing open source project, and therefore do not have the flexibility to restructure the existing codebase a lot. I would ideally have loved to implement this differently, but don't have that option. So posting here to know what options I might have given the above scenario. Thanks.
Upvotes: 2
Views: 3658
Reputation: 800
Firstly, huge thanks to Matt for his detailed explanation. I worked through his example, and expanded on it to work for me as required. The main takeaway for me was: "Move the Collapse children to its own React component."
The solution posted by Matt above, I felt, didn't completely solve the problem for me. E.g. if I add a console.log()
statement to the render()
of the new child component (<Example>
), I still see it being executed before it is mounted.
Adding mountOnEnter
and unmountOnExit
solved this problem:
But as Matt mentioned, the number of times the children were getting rendered was still a problem. So I slightly changed some bits (also simplified the code a bit):
Essentially, I do this now:
My child component is:
function Example(props) {
return (
<div
style={{
fontSize: 100,
textAlign: "center",
color: "white",
backgroundColor: "#43A047"
}}
>
{props.flag && console.log("This should not execute before expanding!")}
{props.value}
</div>
);
}
and I call it from the parent component as:
<TableRow>
<TableCell style={{ paddingBottom: 0, paddingTop: 0 }} colSpan={6}>
<Collapse in={open} timeout="auto" mountOnEnter unmountOnExit>
<Example value={row.name} flag={open} />
</Collapse>
</TableCell>
</TableRow>
Note that the parameter flag
is essential to avoid the function execution during closing of the <Collapse>
.
Upvotes: 1
Reputation: 19762
The children are rendered on initial load because they're defined within the Row
component.
Move the Collapse
children to its own React component. This won't render the children until the Collapse
is opened. However, it'll re-render the child component when Collapse
is closed. So depending on how you're making the API call and how other state interacts with this component, you may want to pass open
to this component and use it as an useEffect
dependency.
For example:
const Example = ({ open }) => {
React.useEffect(() => {
const fetchData = async () => {...};
if(open) fetchData();
}, [open]);
return (...);
}
A separate React component:
const Example = ({ todoId }) => {
const [state, setState] = React.useState({
error: "",
data: {},
isLoading: true
});
const { data, error, isLoading } = state;
React.useEffect(() => {
const fetchData = async () => {
try {
const res = await fetch(
`https://jsonplaceholder.typicode.com/todos/${todoId}`
);
if (res.status !== 200) throw String("Unable to locate todo item");
const data = await res.json();
setState({ error: "", data, isLoading: false });
} catch (error) {
setState({ error: error.toString(), data: {}, isLoading: false });
}
};
fetchData();
/* eslint-disable react-hooks/exhaustive-deps */
}, []);
return (
<div
style={{
textAlign: "center",
color: "white",
backgroundColor: "#43A047"
}}
>
{error ? (
<p style={{ color: "red" }}>{error}</p>
) : isLoading ? (
<p>Loading...</p>
) : (
<>
<div>
<strong>Id</strong>: {data.id}
</div>
<div>
<strong>Title</strong>: {data.title}
</div>
<div>
<strong>Completed</strong>: {data.completed.toString()}
</div>
</>
)}
</div>
);
};
The Example
component being used as children to Collapse
(also see supported Collapse props):
<Collapse
in={open}
timeout="auto"
// mountOnEnter <=== not a supported prop
// unmountOnExit <=== not a supported prop
>
<Example todoId={todoId + 1} />
</Collapse>
If the API data is static and/or doesn't change too often, I'd recommend using data
as a dependency to useEffect
(similar to the open
example above). This will limit the need to constantly query the API for the same data every time the same row is expanded/collapsed.
Upvotes: 1