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

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>
);
};

Related

Expose a Custom Hook to Children (children only) in React

I'm not sure the title is correct, so let me try to explain what I'm trying to achieve.
Let's say I have a flow in my application that has 3 steps in it, so I create a component (let's call it Stepper) with 3 child components where each child is a component that renders the corresponding step.
I want to expose a custom hook to the child components of Stepper, let's call it useStepper.
This is how Stepper would look like (JSX-wise):
export const Stepper = (props) => {
...some logic
return (
<SomeWrapper>
{props.children}
</SomeWrapper>
);
};
so I can make components like this:
export SomeFlow = () => {
return (
<Stepper>
<StepOne />
<StepTwo />
<StepThree />
</Stepper>
);
};
Now this is how I want things to work inside Stepper's children, let's take StepThree as an example:
export const StepThree = () => {
const exposedStepperData = useStepper();
... some logic
return (
...
);
};
Now, it's important that the Stepper will be reusable; That means - each Stepper instance should have its own data/state/context that is exposed through the useStepper hook.
Different Stepper instances should have different exposed data.
Is it possible to achieve this? I tried to use Context API but I was not successful. It's also weird that I couldn't find anything about it on the internet, maybe I searched wrong queries as I don't know what patten it is (if it exists).
Note:
I achieved a similar behavior through injected props from parent to its children, but it's not as clean as I want it to be, especially with Typescript.
I recently came across something like this, it was solved by pouring all the components/steps in an array and let the hook manage which component/step to show. If you want it to be more reusable you could pass in the children to the array.
I hope this helps you in the right direction
useStepper.ts
import { ReactElement, useState } from "react";
export const useStepper = (steps: ReactElement[]) => {
const [currentStepIndex, setCurrentStepIndex] = useState(0);
const next = () => {
setCurrentStepIndex((i: number) => {
if (i >= steps.length - 1) return i;
return i + 1;
});
};
const back = () => {
setCurrentStepIndex((i: number) => {
if (i <= 0) return i;
return i - 1;
});
};
const goTo = (index: number) => {
setCurrentStepIndex(index);
};
return {
currentStepIndex,
step: steps[currentStepIndex],
steps,
isFirstStep: currentStepIndex === 0,
isLastStep: currentStepIndex === steps.length - 1,
goTo,
next,
back,
};
};
Stepper.tsx
// const { currentStepIndex, step, isFirstStep, isLastStep, back, next } =
// useStepper([<StepOne />, <StepTwo />, <StepThree />]);
const { currentStepIndex, step, isFirstStep, isLastStep, back, next } =
useStepper([...children]);
return (
<div>
{!isFirstStep && <button onClick={back}>Back</button>}
{step}
<button onClick={next}>{isLastStep ? "Finish" : "Next"}</button>
</div>
);

Handle button click with two different handler in different condition - ReactJS

I want to handle the click of my custom button component in two separate conditions.
This is the custom-button component:
function CustomButton({status, onStop, onClick}) {
return (
// ...
<button onClick={status ? onStop : onClick}/>
// ...
)
}
This code is just a simple example to explain my question. and I can't separate my component according to the value of status.
My question is, Is this an anti-pattern or bad practice in a component? If yes, what's the best practice to do?
It's not an anti-pattern and more like a bad practice, such code isn't maintainable for when more conditions and callbacks will be added.
Components should be simple, readable and reusable while using all provided props as possible (i.e don't add unused props to component's logic):
const NOOP = () => {}
function CustomButton({ onClick = NOOP }) {
return <button onClick={onClick}>Cool Button</button>;
}
Better practice is to handle condition in parent's logic:
function Parent() {
const onContinue = // ...
const onStop = // ...
return <CustomButton onClick={status ? onStop : onContinue} />;
}
I think that is better to pass a single callback onClick and handle the business logic inside it.
Create a function handleClick inside CustomButton component before return and use if-else blocks to achieve this functionality. It is the right way, according to me.
Create your custom button and use it into any place with any props
import React from 'react';
const CustomButton = ({ children, type = 'button', onClick }) => (
<button
type={type}
onClick={onClick}
>
{children}
</button>
);
const Parent = ({ status }) => {
const stop = useCallback(() => {}, []);
const onClick = useCallback(() => {}, []);
return (
status
? <CustomButton onClick={useCallback}>stop</CustomButton>
: <CustomButton onClick={onClick}>click</CustomButton>
);
}

How to render a different component with React Hooks

I have a parent component with an if statement to show 2 different types of buttons.
What I do, on page load, I check if the API returns an array called lectures as empty or with any values:
lectures.length > 0 ? show button A : show button B
This is the component, called main.js, where the if statement is:
lectures.length > 0
? <div onClick={() => handleCollapseClick()}>
<SectionCollapse open={open} />
</div>
: <LectureAdd dataSection={dataSection} />
The component LectureAdd displays a + sign, which will open a modal to create a new Lecture's title, while, SectionCollapse will show an arrow to show/hide a list of items.
The logic is simple:
1. On page load, if the lectures.lenght > 0 is false, we show the + sign to add a new lecture
OR
2. If the lectures.lenght > 0 is true, we change and show the collpase arrow.
Now, my issue happens when I add the new lecture from the child component LectureAdd.js
import React from 'react';
import { Form, Field } from 'react-final-form';
// Constants
import { URLS } from '../../../../constants';
// Helpers & Utils
import api from '../../../../helpers/API';
// Material UI Icons
import AddBoxIcon from '#material-ui/icons/AddBox';
export default ({ s }) => {
const [open, setOpen] = React.useState(false);
const [ lucturesData, setLecturesData ] = React.useState(0);
const { t } = useTranslation();
const handleAddLecture = ({ lecture_title }) => {
const data = {
"lecture": {
"title": lecture_title
}
}
return api
.post(URLS.NEW_COURSE_LECTURE(s.id), data)
.then(data => {
if(data.status === 201) {
setLecturesData(lucturesData + 1) <=== this doesn't trigger the parent and the button remains a `+` symbol, instead of changing because now `lectures.length` is 1
}
})
.catch(response => {
console.log(response)
});
}
return (
<>
<Button variant="outlined" color="primary" onClick={handleClickOpen}>
<AddBoxIcon />
</Button>
<Form
onSubmit={event => handleAddLecture(event)}
>
{
({
handleSubmit
}) => (
<form onSubmit={handleSubmit}>
<Field
name='lecture_title'
>
{({ input, meta }) => (
<div className={meta.active ? 'active' : ''}>
<input {...input}
type='text'
className="signup-field-input"
/>
</div>
)}
</Field>
<Button
variant="contained"
color="primary"
type="submit"
>
ADD LECTURE
</Button>
</form>
)}
</Form>
</>
)
}
I've been trying to use UseEffect to trigger a re-render on the update of the variable called lucturesData, but it doesn't re-render the parent component.
Any idea?
Thanks Joe
Common problem in React. Sending data top-down is easy, we just pass props. Passing information back up from children components, not as easy. Couple of solutions.
Use a callback (Observer pattern)
Parent passes a prop to the child that is a function. Child invokes the function when something meaningful happens. Parent can then do something when the function gets called like force a re-render.
function Parent(props) {
const [lectures, setLectures] = useState([]);
const handleLectureCreated = useCallback((lecture) => {
// Force a re-render by calling setState
setLectures([...lectures, lecture]);
}, []);
return (
<Child onLectureCreated={handleLectureCreated} />
)
}
function Child({ onLectureCreated }) {
const handleClick = useCallback(() => {
// Call API
let lecture = callApi();
// Notify parent of event
onLectureCreated(lecture);
}, [onLectureCreated]);
return (
<button onClick={handleClick}>Create Lecture</button>
)
}
Similar to solution #1, except for Parent handles API call. The benefit of this, is the Child component becomes more reusable since its "dumbed down".
function Parent(props) {
const [lectures, setLectures] = useState([]);
const handleLectureCreated = useCallback((data) => {
// Call API
let lecture = callApi(data);
// Force a re-render by calling setState
setLectures([...lectures, lecture]);
}, []);
return (
<Child onLectureCreated={handleLectureCreated} />
)
}
function Child({ onLectureCreated }) {
const handleClick = useCallback(() => {
// Create lecture data to send to callback
let lecture = {
formData1: '',
formData2: ''
}
// Notify parent of event
onCreateLecture(lecture);
}, [onCreateLecture]);
return (
<button onClick={handleClick}>Create Lecture</button>
)
}
Use a central state management tool like Redux. This solution allows any component to "listen in" on changes to data, like new Lectures. I won't provide an example here because it's quite in depth.
Essentially all of these solutions involve the same solution executed slightly differently. The first, uses a smart child that notifies its parent of events once their complete. The second, uses dumb children to gather data and notify the parent to take action on said data. The third, uses a centralized state management system.

Using state setter as prop with react hooks

I'm trying to understand if passing the setter from useState is an issue or not.
In this example, my child component receives both the state and the setter to change it.
export const Search = () => {
const [keywords, setKeywords] = useState('');
return (
<Fragment>
<KeywordFilter
keywords={keywords}
setKeywords={setKeywords}
/>
</Fragment>
);
};
then on the child I have something like:
export const KeywordFilter: ({ keywords, setKeywords }) => {
const handleSearch = (newKeywords) => {
setKeywords(newKeywords)
};
return (
<div>
<span>{keywords}</span>
<input value={keywords} onChange={handleSearch} />
</div>
);
};
My question is, should I have a callback function on the parent to setKeywords or is it ok to pass setKeywords and call it from the child?
There's no need to create an addition function just to forward values to setKeywords, unless you want to do something with those values before hand. For example, maybe you're paranoid that the child components might send you bad data, you could do:
const [keywords, setKeywords] = useState('');
const gatedSetKeywords = useCallback((value) => {
if (typeof value !== 'string') {
console.error('Alex, you wrote another bug!');
return;
}
setKeywords(value);
}, []);
// ...
<KeywordFilter
keywords={keywords}
setKeywords={gatedSetKeywords}
/>
But most of the time you won't need to do anything like that, so passing setKeywords itself is fine.
why not?
A setter of state is just a function value from prop's view. And the call time can be anytime as long as the relative component is live.

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

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>
}
}

Resources