React parent component check if child will render - javascript

I have a child component that depending on some of its props will end up rendering something or not. The render function of the children looks something like this:
render() {
if (props.a == 'foo' && props.b == 'bar') {
return (<p> Hey There </p>);
} else if {props.a == 'z') {
return (<p> Hey There </p>);
} // more conditions
} else {
return null
}
}
In the parent component I am rendering several child components, and I need to know how many of them will render, because depending on that number I will do something or not. I don't want to repeat the conditional logic from the child to the parent, but I don't know how from a parent I can find out if the children will render or not.

What you're trying to do is a bit of an anti-pattern in React.
If I understand your question, the children render output would influence their parent's render ouptput, which is likely to get you stuck in a render loop.
I suggest you keep the children components as simple as possible and hoist the conditional logic to the parent which will allow you to count how many children you'll be rendering in place.
Please let me know if I got your question wrong.

Although I agree with perpetualjourney's answer, I thought I could give you a possibility of counting the children.
The easiest would be to save the rendering of the children first to a variable and then to render that result.
var kids = this.props.items.map( (k, i) => <Kid condition={k} key={i} /> );
var totalKids = kids.reduce( (k, i) => k + (i.type( i.props ) ? 1 : 0), 0);
Now, this will render your children twice, so it is not the best when you already have a performance heavy method
const Kid = ({ condition }) => condition % 3 === 0 ? <h1>{ condition }</h1> : null;
class ConditionalKids extends React.Component {
render() {
var kids = this.props.items.map( (k, i) => <Kid condition={k} key={i} /> );
var totalKids = kids.reduce( (k, i) => k + (i.type( i.props ) ? 1 : 0), 0);
return <div>
<p>Rendered in total { totalKids }</p>
{ kids }
</div>;
}
}
const items = [...new Array(5)].map( i => parseInt( Math.random() * 10 ) );
console.log( items );
const target = document.querySelector('#container');
ReactDOM.render( <ConditionalKids items={items} />, target );
<script id="react" src="https://cdnjs.cloudflare.com/ajax/libs/react/15.6.2/react.js"></script>
<script id="react-dom" src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/15.6.2/react-dom.js"></script>
<div id="container"></div>

One way of handling this problem is to use state.
In this CodeSandbox example, I use the if/else logic to update the component's state. This is done inside the componentWillReceiveProps lifecycle method.
Your render method is simplified to a switch statement.
If you want to call an imperative API based on the if/else logic you have, you can do that inside your componentDidUpdate lifecycle method. This is also simplified to a switch statement.

That's a very pertinent question that affects applications of any complexity. Unfortunately there's no clear way to model this using React today. I've done a ton of React and would recommend what #perpetualjourney said: hoist the logic to a common parent. Sometimes you'll need to move it way up, but do it instead of messing around with React.Children.map or React.Context since they'd create dependency between the components and would make it harder for you to move them around if needed.
const hasA = computeHasA({ ... });
const hasB = computeHasB({ ... });
const hasAny = hasA || hasB;
if (hasAny) {
return (
<Tabs>
{hasA && <Tab title="A">Some content</Tab>}
{hasB && <Tab title="B">Another content</Tab>}
</Tabs>
);
}
When you really can't move to logic upwards, for example, if the children do network requests, which is probably an edge-case, I'd recommend passing a prop used to report whether they have content or not and storing it into the parent's state:
const [numberOfRenderedChildren, setNumberOfRenderedChildren] = useState(0);
const incrementNumberOfRenderedChildren = () => {
// Use a callback to prevent race conditions
setNumberOfRenderedChildren(prevValue => prevValue + 1);
};
return (
<MyContainer isVisible={numberOfRenderedChildren > 0}>
{items.map(item => (
<ComplexChild
key={item.id}
// Avoid spreading props like ...item. It breaks semantics
item={item}
reportHasContent={incrementNumberOfRenderedChildren}
/>
)}
</MyContainer>
);
It could get more complex if you need loaders and error messages, but then I'd fire all the requests in the parent. Try to keep it simple, easy to read, even if it means more verbosity. You won't regret when you need to come back to those files a few weeks later.

Related

React.js State updating correctly only once, then failing [duplicate]

This question already has answers here:
Using a Set data structure in React's state
(2 answers)
Closed 1 year ago.
I have a parent and a child component. There are 3 props the parent provides out of which 1 is not updating correctly.
Following is the parent component. The prop in question is selectedFilters (which is an object where keys are mapped to sets) and the relevant update function is filterChanged (this is passed to the child)
import filters from "../../data/filters"; //JSON data
const Block = (props) => {
const [selectedFilters, setSelectedFilters] = useState({versions: new Set(), languages: new Set()});
console.log(selectedFilters);
const filterChanged = useCallback((filter_key, filter_id) => {
setSelectedFilters((sf) => {
const newSFSet = sf[filter_key]; //new Set(sf[filter_key]);
if (newSFSet.has(filter_id)) {
newSFSet.delete(filter_id);
} else {
newSFSet.add(filter_id);
}
const newSF = { ...sf, [filter_key]: new Set(newSFSet) };
return newSF;
});
}, []);
return (
<FilterGroup
filters={filters}
selectedFilters={selectedFilters}
onFilterClick={filterChanged}
></FilterGroup>
);
};
export default Block;
The following is the child component: (Please note that while the Filter component runs the filterChanged function, I think it is irrelevant to the error)
import Filter from "./Filter/Filter";
const FilterGroup = (props) => {
const { filters, selectedFilters, onFilterClick } = props;
console.log(selectedFilters);
const filter_view = (
<Container className={styles.container}>
{Object.keys(filters).map((filter_key) => {
const filter_obj = filters[filter_key];
return (
<Filter
key={filter_obj.id}
filter_key={filter_key}
filter_obj={filter_obj}
selectedFilterSet={selectedFilters[filter_key]}
onFilterClick={onFilterClick}
/>
);
})}
</Container>
);
return filter_view;
};
export default FilterGroup;
When running the application, I find that the selectedFilters updates correctly only once. After that, it only changes temporarily in the main Block.tsx, but eventually goes back to the first updated value. Also, FilterGroup.tsx only receives the first update. After that, it never receives any further updated values.
Here are the logs:
After some experimentation, it is clear that the problem originates from the filterChanged function. But I cannot seem to figure out why the second update is temporary AND does not get passed on to the child.
Any ideas? Thanks in advance.
(If any other info is required, pls do mention it)
I don't think you actually want your filterChanged function to be wrapped with useCallback, especially with an empty deps array. with the empty deps array, I believe useCallback will fire once on initial render, and memoize the result. You may be able to add filter_key and filter_id to the dependency array, but useCallback tends to actually slow simple functions down, instead of adding any real performance benefit, so you may just want to get rid of the useCallback completely and switch filterChanged to a regular arrow function.

An object component

I'm learning React and I'm trying to render a form with a few different form-steps (details, payments, confirm...) from a UI library that has Form component ready, which iterates through those form-steps.
The issue is that each 'step' component looks more like an actual JS object:
export const DetailStep = (
<div>Details screen with inputs, texts and images...</div>
);
It doesn't have the = () => so it's not an actual functional component, so I can't add hooks or functions into it.
That's the array of steps:
const stepPages = [
DetailStep,
PaymentStep,
ConfirmStep
];
And that's the actual Form component that iterates through the array of steps:
<Form
onSubmitClick={onStepSubmit}
render={formRenderProps => {stepPages[step]} }
/>
If I'll change any step to a functional component (DetailStep = () => ...), the step will be blank and won't be rendered.
How can I fix that / just add hooks to each 'step' component?
JSX declarations are converted into React.createElement function calls, this does mean that the stepPages in your example are objects (just that they are react objects)
The magic that turns them into function components happens in the render prop of the form:
<Form
onSubmitClick={onStepSubmit}
render={formRenderProps => {stepPages[step]} }
/>
You can see that it's passing a function in here, that returns the jsx objects. This is kinda the same as putting the jsx directly in the render function.
Although I notice that in your example, the jsx isn't being returned, so I would expect this example to not work correctly.
The longform equivalent would be something like this:
<Form
onSubmitClick={onStepSubmit}
render={formRenderProps => {
if (step === 0) {
return (
<div>Details screen with inputs, texts and images...</div>
)
} else if (step === 1) {
return (
<div>Details screen with inputs, texts and images...</div>
)
} // etc
}}
/>
EDIT to answer question in comments
You're right, so how can I add hooks into those object components?
If each of the form steps needs to use hooks and manage it's internal state, then I would make them functional components instead of JSX objects, then pick the right component from the stepPages map and render it like so:
export const DetailStep = (props) => {
// hooks and stuff here
return (
<div>Details screen with inputs, texts and images...</div>
);
}
const StepComponent = stepPages[step];
return (
<Form
onSubmitClick={onStepSubmit}
render={formRenderProps => <StepComponent {...formRenderProps} />}
/>
);

React Hooks (Rendering Arrays) - Parent component holding a reference of children that are mapped vs Parent component holding the state of children

I have been learning hooks in react for the past couple of days, and I tried creating a scenario where I need to render a big grid on screen, and update the background color of the nodes depending on the action I want to take. There are two actions that will change the background color of a node, and these two actions must coexist.
The cursor hovers a node while it is clicked.
There exists an algorithm inside the Grid component that will change backgrounds of some of the
nodes.
The way I see it, there are multiple ways I can achieve this, but I am having some trouble with the way hooks were intended to be used. I will first walk you through my thought process on how this could be achieved from what I learned, and then show you the implementation that I tried. I tried to keep the important parts of the code so it can be understood clearly. Please let me know if I missed somethings or misunderstood a concept completely.
The children can hold their own state and know how to update themselves. The parent can hold the reference to each children of the list, and call the necessary function from the reference of the child when it is needed in order to update the children.
Works well for the first and the second action to be taken. This solution causes no performance issues since the children manage their own state, and if the parent updates the children state via reference, the only child to be re-rendered will be the one that gets called.
This solution is seen as an anti-pattern from what I read.
const Grid = () => {
// grid array contains references to the GridNode's
function handleMouseDown() {
setIsMouseDown(true);
}
function handleMouseUp() {
setIsMouseDown(false);
}
function startAlgorithm() {
// call grid[row][column].current.markAsVisited(); for some of the children in grid.
}
return (
<table>
<tbody>
{
grid.map((row, rowIndex) => {
return (
<tr key={`R${rowIndex}`}>
{
row.map((node, columnIndex) => {
return (
<GridNode
key={`R${rowIndex}C${columnIndex}`}
row={rowIndex}
column={columnIndex}
ref={grid[rowIndex][nodeIndex]}
onMouseDown={handleMouseDown}
onMouseUp={handleMouseUp}
/>
);
})
}
</tr>
);
}
)
}
</tbody>
</table>
);
};
const GridNode = forwardRef((props, ref) => {
const [isVisited, setIsVisited] = useState(false);
useImperativeHandle(ref, () => ({
markAsVisited: () => {
setIsVisited(!isVisited);
}
}));
function handleMouseDown(){
setIsVisited(!isVisited);
}
function handleMouseEnter () {
if (props.isMouseDown.current) {
setIsVisited(!isVisited);
}
}
return (
<td id={`R${props.row}C${props.column}`}
onMouseDown={handleMouseDown}
onMouseEnter={handleMouseEnter}
className={classnames("node", {
"node-visited": isVisited
})}
/>
);
});
2. The state of the children could be given as props from the parent, any update operation can be achieved inside the parent. (Children gets updated correctly, render gets called in only the necessary children, but the DOM seems to stutter. If you move the mouse at a certain speed, nothing happens, and every visited node gets updated at once.)
Doesn't work for the first action. Children gets updated correctly, render gets called in only the necessary children, but the DOM seems to stutter. If you move the mouse at a certain speed, nothing happens and every visited node gets updated at once.
const Grid = () => {
// grid contains objects that have boolean "isVisited" as a property.
function handleMouseDown() {
isMouseDown.current = true;
}
function handleMouseUp() {
isMouseDown.current = false;
}
const handleMouseEnterForNodes = useCallback((row, column) => {
if (isMouseDown.current) {
setGrid((grid) => {
const copyGrid = [...grid];
copyGrid[row][column].isVisited = !copyGrid[row][column].isVisited;
return copyGrid;
});
}
}, []);
function startAlgorithm() {
// do something with the grid, update some of the "isVisited" properties.
setGrid(grid);
}
return (
<table>
<tbody>
{
grid.map((row, rowIndex) => {
return (
<tr key={`R${rowIndex}`}>
{
row.map((node, columnIndex) => {
const {isVisited} = node;
return (
<GridNode
key={`R${rowIndex}C${columnIndex}`}
row={rowIndex}
column={columnIndex}
isVisited={isVisited}
onMouseDown={handleMouseDown}
onMouseUp={handleMouseUp}
onMouseEnter={handleMouseEnterForNodes}
/>
);
})
}
</tr>
);
}
)
}
</tbody>
</table>
);
};
const GridNode = ({row, column, isVisited, onMouseUp, onMouseDown, onMouseEnter}) => {
return useMemo(() => {
function handleMouseEnter() {
onMouseEnter(props.row, props.column);
}
return (
<td id={`R${row}C${column}`}
onMouseEnter={handleMouseEnter}
onMouseDown={onMouseDown}
onMouseUp={onMouseUp}
className={classnames("node", {
"node-visited": isVisited
})}
/>
);
}, [props.isVisited]);
}
I have two questions that I want to ask on this topic.
In the first implementation; the parent component doesn't re-render when a node changes its' state. Is it wrong to just utilize this anti-pattern if it is beneficial in this kind of situations?
What may be the cause of the stutter that the second implementation suffers from? I have spent a while reading the docs and trying out different things, but cannot find the reason of the stuttering that is happening.
As you say that using refs to control child data is an anti-pattern, However it doesn't mean that you cannot use it.
What it means is that if there are better and more performant means, its better to use them as they lead to better readability of the code and also improve debugging.
In your case using a ref definitely makes it easier to update state and also prevents a lot of re-rendering is a good way to implement the above solution
What may be the cause of the stutter that the second implementation suffers from? I have spent a while reading the docs and trying out different things, but cannot find the reason of the stuttering that is happening.
A lot of the problem in the second solution arise from the fact that you define functions which are recreated on each re-render and hence cause the entire grid to be re-rendered instead of just the cell. Make use of useCallback to memoize these function in Grid component
Also you should use React.memo instead of useMemo for your usecase in GridNode.
Another thing to note is that you are mutating the state while updating, Instead you should update it in an immutable manner
Working code:
const Grid = () => {
const [grid, setGrid] = useState(getInitialGrid(10, 10));
const isMouseDown = useRef(false);
const handleMouseDown = useCallback(() => {
isMouseDown.current = true;
}, []);
const handleMouseUp = useCallback(() => {
isMouseDown.current = false;
}, []);
const handleMouseEnterForNodes = useCallback((row, column) => {
if (isMouseDown.current) {
setGrid(grid => {
return grid.map((r, i) =>
r.map((c, ci) => {
if (i === row && ci === column)
return {
isVisited: !c.isVisited
};
return c;
})
);
});
}
}, []);
function startAlgorithm() {
// do something with the grid, update some of the "isVisited" properties.
setGrid(grid);
}
return (
<table>
<tbody>
{grid.map((row, rowIndex) => {
return (
<tr key={`R${rowIndex}`}>
{row.map((node, columnIndex) => {
const { isVisited } = node;
if (isVisited === true) console.log(rowIndex, columnIndex);
return (
<GridNode
key={`R${rowIndex}C${columnIndex}`}
row={rowIndex}
column={columnIndex}
isVisited={isVisited}
onMouseDown={handleMouseDown}
onMouseUp={handleMouseUp}
onMouseEnter={handleMouseEnterForNodes}
/>
);
})}
</tr>
);
})}
</tbody>
</table>
);
};
const GridNode = ({
row,
column,
isVisited,
onMouseUp,
onMouseDown,
onMouseEnter
}) => {
function handleMouseEnter() {
onMouseEnter(row, column);
}
const nodeVisited = isVisited ? "node-visited" : "";
return (
<td
id={`R${row}C${column}`}
onMouseEnter={handleMouseEnter}
onMouseDown={onMouseDown}
onMouseUp={onMouseUp}
className={`node ${nodeVisited}`}
/>
);
};
P.S. While useCallback and other memoizations will help give to some performance benefits it will still not be able to overcome the performance impacts on state updates and re-render. In such scenarios its better to make define state within the children and expose a ref for the parent
As stated, the solution is an anti-pattern because you're mixing the rendering and business logic on both levels. You don't need to explicitly use React.forwardRef, in fact according to the docs you shouldn't, even when composing HOC (Higher order components). You shouldn't need to directly access the element and do some sort of action on it - let React do its thing. It's very good and efficient at it.
Generally when you're calling a re-render method on a child node tree when there's n nodes, you don't want to cause a re-render from the top-level node, the parent in this case, because it will cause the entire node-tree to re-render into a new DOM element, rather than update existing elements.
Your current solution has a combination of parent-triggered renders and child triggered renders. The React page has a good example with the tic-tac-toe application for how to render children without causing the parent to re-render.
The strategy that you should use is one where the parent node has an object structure, in this case n^2 nodes (eg 10x10 for arguments sake), is to pass the rendering functionality to the child nodes, and let the child nodes handle the rendering.
When you're triggering a render from the parent node, you have a couple of options (assuming functional components) which really fall into the case of observable updates. You want to be able to push updates from the parent to the child, to modify the child node state, and let the child node update itself.
Here's an example with child nodes rendering, while the parent is communicating changes to the children. You'll see that the performance scales well even up to massive grids, compared to the nested level renders your example has.
https://codepen.io/jmitchell38488/pen/pogbKEb
This is achieved by using a combination of RxJS observable/subject, React.useState and React.useEffect. We use useState in both the parent and child nodes to deal with rendering and prop updates, and useEffect to bind the observable. useState is persistent between renders, which means you don't need to rebuild the entire grid every time you update in the parent, but even if you do, React is intelligent enough to determine that you updated the props of a node, not replaced it.
const Grid = (props) => {
// When we update the grid, we trigger the parent to re-render
const [grid, setGrid] = React.useState([]);
const subject = new Rx.Subject();
if (grid.length < 1) {
const newGrid = [];
for (i = 0; i < props.h; i++) {
for (k = 0; k < props.w; k++) {
if (!Array.isArray(newGrid[i])) {
newGrid[i] = [];
}
newGrid[i][k] = {
visited: false,
id: `${i}${k}`
};
}
}
setGrid(newGrid);
}
// Tell our node to update
handleClick = (node, visited) => {
subject.next({
id: node.id,
visited: visited
})
};
randomSetAllVisited = () => {
const newGrid = [...grid];
newGrid.forEach(row => {
row.forEach(node => {
node.visited = Math.random() * 2 >= 1;
})
})
// Tell parent to re-render
setGrid(newGrid);
// Because our nodes use `useState`, they are persistent, if the structure of
// grid is the same and the data is mostly the same. This is based on the `key={...}` value
// in row.map, so we need to tell our children nodes to re-render manually
subject.next({
reset: true
})
};
randomSetAnyVisited = () => {
const h = Math.floor(Math.random()*props.h);
const w = Math.floor(Math.random()*props.w);
const node = grid[h][w];
subject.next({
id: node.id,
visited: true
});
};
// Watch console.log to see how frequently parent renders
console.log("rendering parent");
return (
<div>
<table>
<tbody>
{grid.map((row, rowIndex) => (
<tr key={`R${rowIndex}`}>
{row.map((node, columnIndex) => (<GridNode {...node} observer={subject.asObservable()} key={node.id} />))}
</tr>
))}
</tbody>
</table>
<button onClick={randomSetAllVisited}>Random set all visited</button>
<button onClick={randomSetAnyVisited}>Random set any visited</button>
</div>
);
};
const GridNode = (props) => {
// We need to set to undefined to handle full grid reset from parent
const [visited, setVisited] = React.useState(undefined);
// Toggle visited based on props and if visited is undefined
if (props.visited !== visited && visited === undefined) {
setVisited(props.visited);
}
// bind all this with useEffect, so we can subscribe/unsubscribe, and not block rendering, `useEffect` is a good practice
React.useEffect(() => {
// notifications that come from parent node, `setVisited` will re-render this node
const obs = props.observer.subscribe(next => {
if (!!next.id && next.id === props.id) {
setVisited(next.visited !== undefined ? next.visited : !visited);
} else if (!!next.reset) {
setVisited(undefined);
}
});
return () => obs.unsubscribe();
}, [visited]);
handleMouseEnter = () => {
setVisited(!visited);
}
handleMouseLeave = () => {
setVisited(!visited);
}
classes = ["node"];
if (visited) {
classes.push("node-visited");
}
return (<td onMouseEnter={handleMouseEnter} onMouseLeave={handleMouseLeave} className={classes.join(" ")}/>);
}
In the codepen example, I have a 50x50 grid, that has no stutters, lag, or issues re-rendering the children nodes, or updating them. There are two helper buttons to randomise the state for all nodes, or randomise a single node. I've scaled this over 100x100 and no lag or performance issues.

React conditional sorting of array

I am getting a value from redux state and I wish to sort an array based conditionally on what I get from the state.
The current way I've written this is as follows:
class ProductsPage extends Component {
state = {
productArrayFromBackend: [],
productArrayToDisplay: []
}
render() {
let ProductArrayToProcess = []
if (this.props.ProductFilter == "latest") {
ProductArrayToProcess = this.state.productArrayToDisplay.sort((a, b) => (a.addedOn > b.addedOn ? -1 : 1))
} else if (this.props.ProductFilter == "popular") {
ProductArrayToProcess = this.state.productArrayToDisplay.sort((a, b) => (a.likes > b.likes ? -1 : 1))
}
else {
ProductArrayToProcess = this.state.productArrayToDisplay
}
return (
<figure>
<div className="product-warp">
{ProductArrayToProcess.map((product, i) => <ProductThumbnailElement key={i} Product={product} />)}
</div>
</figure>
);
}
}
Now, the value for this.props.ProductFilter is coming from state. What I wish to do is when the value in the redux state changes, it should change the value that is rendered inside the jsx too.
Please let me know how is that possible?
You could simply extract the your required data inside the render function.
render(){
const {ProductFilter} = this.props;
// use ProductFilter in the following code using JSX
}
Simply declaring the variable once at the begging is a better practice than calling this.props.X several time.
Furthermore, instead of calling the change items from the state directly, you might want to copy their value, and make changes upon this copy.
BTW, I'll suggest to export the ProductArrayToProcess generation to a matching function, there of course you'll still have the access of props and state.

this.props is not a function in the componentDidUpdate()

I'm facing a weird problem, there's probably a simple thing I didn't noticed but I'm not able to pass through this bug. Here's my case:
In the child, a counter is updated onClick. What I'm trying to do is fire a props function to togle the state of the parent.
In the child:
componentDidUpdate(){
if(this.state.counter === 3){
return this.props.isFullBackgroundHomePage();
}else{
return;
}
}
In the parent:
renderHomePage = () => {
if(this.state.isHomePage
&& this.state.home_page !== null){
return (
<HomePage
isFullBackgroundHomePage={this.isFullBackgroundHomePage}
triggerSeeAllSection={this.triggerSeeAllSection}
{...this.state}
/>
)
}else{
return null;
}
}
isFullBackgroundHomePage = () => {
this.setState({
isFullBackgroundHomePage: !this.state.isFullBackgroundHomePage
})
}
Now, this is what appears in the browser when the counter is at 3:
TypeError: this.props.isFullBackgroundHomePage is not a function
Link to the browser log: https://ibb.co/vcMW3wL
In
<HomePage
isFullBackgroundHomePage={this.isFullBackgroundHomePage}
triggerSeeAllSection={this.triggerSeeAllSection}
{...this.state}
/>
you are overwriting prop isFullBackgroundHomePage using {...this.state} thus its not a function anymore rather a true/false boolean value at the consuming component. In your consuming component if you need both, you should rename state or prop value.
Very common issue unintentionally overwriting props using spread, gets the best of developer musing around what the heck is going on but to avoid such issues, consider using separate naming for function and state value to separate concerns.

Categories

Resources