Why React renders props.children even when it can be avoided? - reactjs

How exactly react works with children props?
An example below Demo component will fail with NPE on the someArray iteration when it is null. Idea behind Wrapper component is to protect the case when there is no data - here I just show message 'working...' but normally it can be extended to anything, spinner for example, or progress bar. Also, in this example, even if I'll fix it with:
{someArray && someArray.map((arrayValue) => <li>{arrayValue}</li>)}
In case of someArray null react WILL do unnecessary rendering of everyhing inside Wrapper which is not very smart.
The problem as I see it is that react always renders children of Wrapper component, even if it can skip it. What stops react from having children as function that does render it content on demand instead?
const Demo = () => {
const someArray = null; // will fail with NPE
//const someArray = ['aaa', 'bbb', 'ccc']; // will render content
return(
<Wrapper isWorking={someArray === null}>
<h1>Inside</h1>
<ul>
{someArray.map((arrayValue) => <li>{arrayValue}</li>)}
</ul>
</Wrapper>
);
}
const Wrapper = ({ isWorking, children }) => {
if( isWorking ){
return <div>working...</div>
}else{
return <div>{children}</div>
}
}

Related

React createProtal called outsite a JSX component not updating the DOM

I am trying to render a dynamically generated react component in a react app using createProtal.
When I call createProtal from a class the component is not rendered.
Handler.ts the class the contains the business logic
export class Handler {
private element: HTMLElement | null;
constructor(selector: string) {
this.element = document.getElementById(selector);
}
attachedEvent() {
this.element?.addEventListener("mouseenter", () => {
let cancel = setTimeout(() => {
if (this.element != null)
this.attachUi(this.element)
}, 1000)
this.element?.addEventListener('mouseleave', () => {
clearTimeout(cancel)
})
})
}
attachUi(domNode: HTMLElement) {
createPortal(createElement(
'h1',
{className: 'greeting'},
'Hello'
), domNode);
}
}
Main.tsx the react component that uses Handler.ts
const handler = new Handler("test_comp");
export default function Main() {
useEffect(() => {
// #ts-ignore
handler.useAddEventListeners();
});
return (
<>
<div id="test_comp">
<p>Detect Mouse</p>
</div>
</>
)
}
However when I repleace attachUi function with the function below it works
attachUi(domNode: HTMLElement) {
const root = createRoot(domNode);
root.render(createElement(
'h1',
{className: 'greeting'},
'Hello'
));
}
What am I missing?
React uses something called Virtual DOM. Only components that are included in that VDOM are displayed to the screen. A component returns something that React understands and includes to the VDOM.
createPortal(...) returns exactly the same as <SomeComponent ... />
So if you just do: const something = <SomeComponent /> and you don't use that variable anywhere, you can not display it. The same is with createPortal. const something = createPortal(...). Just use that variable somewhere if you want to display it. Add it to VDOM, let some of your components return it.
Your structure is
App
-children
-grand children
-children2
And your portal is somewhere else, that is not attached to that VDOM. You have to include it there, if you want to be displayed.
In your next example using root.render you create new VDOM. It is separated from your main one. This is why it is displayed

How should I update individual items' className onClick in a list in a React functional component?

I'm new to React and I'm stuck trying to get this onClick function to work properly.
I have a component "Row" that contains a dynamic list of divs that it gets from a function and returns them:
export function Row({parentState, setParentState}) {
let divList = getDivList(parentState, setParentState);
return (
<div>
{divList}
</div>
)
}
Say parentState could just be:
[["Name", "info"],
["Name2", "info2"]]
The function returns a list of divs, each with their own className determined based on data in the parentState. Each one needs to be able to update its own info in parentState with an onClick function, which must in turn update the className so that the appearance of the div can change. My code so far seems to update the parentState properly (React Devtools shows the changes, at least when I navigate away from the component and then navigate back, for some reason), but won't update the className until a later event. Right now it looks like this:
export function getDivList(parentState, setParentState) {
//parentState is an array of two-element arrays
const divList = parentState.map((ele, i) => {
let divClass = "class" + ele[1];
return (
<div
key={ele, i}
className={divClass}
onClick={() => {
let newParentState =
JSON.parse(JSON.stringify(parentState);
newParentState[i][1] = "newInfo";
setParentState(newParentState);}}>
{ele[0]}
</div>
)
}
return divList;
}
I have tried to use useEffect, probably wrong, but no luck. How should I do this?
Since your Row component has parentState as a prop, I assume it is a direct child of this parent component that contains parentState. You are trying to access getDivList in Row component without passing it as a prop, it won't work if you write your code this way.
You could use the children prop provided by React that allow you to write a component with an opening and closing tag: <Component>...</Component>. Everything inside will be in the children. For your code it would looks like this :
import React from 'react';
import { render } from 'react-dom';
import './style.css';
const App = () => {
const [parentState, setParentState] = React.useState([
['I am a div', 'bg-red'],
['I am another div', 'bg-red'],
]);
React.useEffect(
() => console.log('render on ParentState changes'),
[parentState]
);
const getDivList = () => {
return parentState.map((ele, i) => {
return (
<div
key={(ele, i)}
className={ele[1]}
onClick={() => {
// Copy of your state with the spread operator (...)
let newParentState = [...parentState];
// We don't know the new value here, I just invented it for the example
newParentState[i][1] = [newParentState[i][1], 'bg-blue'];
setParentState(newParentState);
}}
>
{ele[0]}
</div>
);
});
};
return <Row>{getDivList()}</Row>;
};
const Row = ({ children }) => {
return <>{children}</>;
};
render(<App />, document.getElementById('root'));
And a bit of css for the example :
.bg-red {
background-color: darkred;
color: white;
}
.bg-blue {
background-color:aliceblue;
}
Here is a repro on StackBlitz so you can play with it.
I assumed the shape of the parentState, yu will have to adapt by your needs but it should be something like that.
Now, if your data needs to be shared across multiple components, I highly recommand using a context. Here is my answer to another post where you'll find a simple example on how to implement a context Api.

Making the state of a component affect the rendering of a sibling when components are rendered iteratively

I have the following code:
export default function Parent() {
const children1 = someArrayWithSeveralElements.map(foo => <SomeView />);
const children2 = someArrayWithSeveralElements.map(foo => <SomeCheckbox />);
return (<>
{children1}
{/*Some other components*/}
{children2}
</>)
};
For a given element foo, there is a SomeView component that is conditionally rendered based on the state of a SomeCheckbox. I'm having trouble figuring out a way to have the state from the checkbox affect the rendering of the sibling view component.
Normally the solution would be to just declare the state hook in the parent component and pass them down to each child, but since the siblings are rendered via foreach loops it's impossible to do so.
My current solution is to also generate the state hooks for each foo in a loop as well, but that feels a bit hacky since it's better to avoid creating hooks inside of loops (it's worth nothing that someArrayWithSeveralElements is not intended to change after mounting).
Is there a more elegant alternative to solve this?
The solution is what you side, you need to create a state in the parent component and pass it to the children. and this will work for single component or bunch of them, the difference is just simple: use array or object as state.
const [checkboxesStatus, setCheckboxesStatus] = useState({// fill initial data});
const children1 = someArrayWithSeveralElements.map(foo =>
<SomeView
visibile={checkBoxesStatus[foo.id]}
/>);
const children2 = someArrayWithSeveralElements.map(foo =>
<SomeCheckbox
checked={checkBoxesStatus[foo.id]}
onChange={// set new value to foo.id key}
/>)
export default function Parent() {
const [states, setStates] = React.useState([]);
const children1 = someArrayWithSeveralElements.map((foo, i) => <SomeView state={states[i]} />);
const children2 = someArrayWithSeveralElements.map((foo, i) => {
const onStateChange = (state) => {
setStates(oldStates => {
const newStates = [...(oldStates || [])]
newStates[i] = state;
return newStates;
})
}
return <SomeCheckbox state={states[i]} onStateChange={onStateChange} />;
});
return (<>
{children1}
{/*Some other components*/}
{children2}
</>)
};
Use states in the parent componet.
Note: the element of states may be undefined.

React - Predicate functional component

I created a react functional component to wrap sections of code based on a predicate (instead of using ternary within JSX).
So, the component looks like this:
const PredicateWrapper = ({ children, iff, otherwise }) => (
iff ? children : (otherwise || null)
);
And can be used like this:
<PredicateWrapper iff={students}>
{students.map(student => <Student />)}
</PredicateWrapper>
The issue is that this throws an error "cannot read property map of null" if students is null. If I replace the students.map with some text, the text does not render, proving the wrapper is working, however, the point of the wrapper is to deal with cases where students is null, and I would expect it not to "enter" inside the wrapper.
How can I achieve something like this that allows for the inner code to not evaluate and throw runtime errors if iff is falsy?
Really interesting question!
I could be woefully wrong here but I think it's because students.map(student => <Student /> is still part of your parent component's render tree. So when React tries to build the parent component's tree, it's trying to execute that line which throws an error.
PredicateWrapper is mounted and resolved when the child components are recursed upon, which is when the iff would kick in - but that's on the second iteration.
For eg, if I do this, I don't get the error
const Child = ({ students }) => {
return students.map(student => <Student />);
};
<PredicateWrapper iff={students}>
<Child students={students} />
</PredicateWrapper>
Let's have a look at how the JSX will be compiled to JavaScript with the online Babel compiler:
const PredicateWrapper = ({ children, iff, otherwise }) =>
iff ? children : otherwise || null;
const students = null;
const App = () =>
React.createElement(
PredicateWrapper,
{
iff: students,
},
students.map((student) => React.createElement(Student, null))
);
Now you can see students.map will be executed and cause the error "Cannot read property map of null"
One possible implementation to achieve your goal is to use render props:
The term “render prop” refers to a technique for sharing code between
React components using a prop whose value is a function.
const PredicateWrapper = ({ render, iff, otherwise = () => null }) =>
iff ? render() : otherwise();
const StudentA = () => <span>😎</span>;
const StudentB = () => <span>😂</span>;
const students = [StudentA, StudentB];
//const students = null;
function App() {
return (
<PredicateWrapper
iff={students}
render={() => students.map(Student => <Student />)}
/>
);
}

React.Children does not detect when child(ren) is null

I have read the various related issues on this subject, but none of them provide me with a solution.
my child is defined as follows
const RevocationHandler = ({ onClose = noop }) => {
const revocation = useRevocationContext();
const [isOpen, toggleOpen] = useState(false);
if (revocation.onRevoke == null) {
console.log('I am null');
return null;
}
function handleButtonClick (evt: React.MouseEvent): void {
evt.stopPropagation();
toggleOpen(true);
}
function handleClose () {
toggleOpen(false);
onClose();
}
if (!isOpen) {
return (
<button
onClick={handleButtonClick}
>
Revoke
</button>
);
} else {
return (
<Modal onClose={handleClose} useTransparency={true}>
<RevocationForm onCancel={handleClose} />
</Modal>
);
}
};
Please note that it's using Context to retrieve a callback function, and if that function is undefined, it should return null.
The parent is defined as such:
<ActionMenu>
<RevocationHandler />
</ActionMenu>
I would like the ActionMenu not to render if all (in this case only 1) children are null.
I have tried various variations of the following code:
React.Children
.toArray(children)
.filter((child) => React.isValidElement(child))
.length
With count, with filter, with map, whatever I try, at this moment of the execution React tells me I have 1 child. Yet, when things run, I do get the I am null console log:
As well as nothing rendered:
My question is, how do I properly detect that my Child will be null in this case?
At the first place what do you mean saying I would like the ActionMenu not to render ? If ActionMenu consists only of children and every child is null then nothing will be rendered anyway...
At second place that is the main ideology of react - all data goes from parents to children, so yopu can not rely on anything which children will render, then everything is upsidedown - children should depend on parent information...
So the approach is basically wrong i guess.
So, I have gone with the "React" way. It breaks encapsulation as it leaks concerns where it shouldn't, and eventually I still read the Children.count option (which is provided by React). And now it works. Less cleanly.
inAnotherFileThatShouldNotNeedToKnow.ts:
<ActionMenu>
{canRevoke && <RevocationHandler />}
</ActionMenu>
ActionMenu.ts:
if (React.Children.count(children) === 0) {
return null;
}
It's a pity that there is no better way to know that a child will return null, when things like React.Children.toArray does filter null children for you, but I can move on with my life as it serves my purpose: the ActionMenu is smart enough to not render when it has nothing to render.
Don't just believe in React dogma, people.
Ideally, data should flow from parent to child. You can move context checks to the parent component such that you have control over whether to render the children or not.
The child component will take care of its core functionality (like to show a modal on click of a button) and the parent component will decide based on checks when to render the child components.
const RevocationHandler = ({ onClose = noop }) => {
const [isOpen, toggleOpen] = useState(false);
function handleButtonClick (evt: React.MouseEvent): void {
evt.stopPropagation();
toggleOpen(true);
}
function handleClose () {
toggleOpen(false);
onClose();
}
if (!isOpen) {
return (
<button
onClick={handleButtonClick}
>
Revoke
</button>
);
} else {
return (
<Modal onClose={handleClose} useTransparency={true}>
<RevocationForm onCancel={handleClose} />
</Modal>
);
}
};
const ActionMenu = ({children}) => {
// you can add a check here if children should be rendered
const revocation = useRevocationContext();
if (revocation.onRevoke == null) {
console.log('I am null');
return null;
}
return (
<div>
....
{children}
</div>
);
};

Resources