Destructure React component into "building blocks" - reactjs

I am using IonSlides in my app but due to a bug with them, dynamically adding slides can prove difficult.
Because IonSlides is built upon SwiperJS, it has some methods to add and remove slides. The downside to those is that they take a string with HTML in it. In my case, I need to be able to pass in JSX elements so that I can use event listeners on them. Originally, this was my code:
private bindEvents(el: JSX.Element): void {
if (el.props.children) { //Checking if the element actually has children
const children = this.toArray(el.props.children); //If it has only 1 child, it is an object, so this just converts it to an array
children.forEach((c: any) => {
if (!c.props) return; //Ignore if it has no props
const propNames = this.toArray(Object.keys(c.props)); //Get the key names of the props of the child
const el = $(`.${c.props.className}`); //Find the element in the DOM using the class name of the child
propNames.forEach(p => { //Binds the actuall events to the child.
if (Events[p] !== undefined) {
el.on(Events[p], c.props[p]); //`c.props[p]` is the handler part of the event
}
});
});
}
}
Which was called through:
appendSlide(slides: JSX.Element | JSX.Element[]): void {
if (this.slideRef.current === null) return;
this.slideRef.current.getSwiper().then(sw => {
slides = this.toArray(slides);
slides.forEach(s => {
sw.appendSlide(ReactDOMServer.renderToString(s));
this.bindEvents(s);
});
});
}
This worked perfectly when appendSlide was called with an IonSlide:
x.appendSlide(<IonSlide>
<div onClick={() => console.log("Clicked!")}</div>Click me!</IonSlide>
If you clicked the div, it would print "Clicked!".
However, if you pass in a custom component, it breaks. That is because the custom component does not show the children under props. Take this component:
interface Props {
test: string,
}
const TestSlide: React.FC<Props> = (props) => {
return (
<IonSlide>
<div>
{props.string}
</div>
</IonSlide>
);
}
If you were to print that component's props, you get:
props: {test: "..."}
rather than being able to access the children of the component, like I did in the bindEvents function.
There's two ways that I could do fix this. One is getting the JS object representation of the component, like this (I remember doing this ages ago by accident, but I can't remember how I got it):
{
type: 'IonSlide',
props: {
children: [{
type: 'div',
props: {
children: ["..."],
},
}
},
}
or, a slight compromise, destructuring the custom component into its "building blocks". In terms of TestSlide that would be destructuring it into the IonSlide component.
I been trying out things for a few hours but I haven't done anything successful. I would really appreciate some help on this.

For whatever reason that someone needs this, I found you can do el.type(el.props) where el is a JSX element.
This creates an instance of the element so under children instead of seeing the props, you can see the actual child components of the component.

Related

Specify a number of children components is required using PropTypes (React)?

Is there a way to specify a required number of children components in a custom React component? I have made a component that is supposed to take exactly two child components that are UserInput components (which I have defined elsewhere) and I would like to specify this using PropTypes. My attempt is below.
/* Renders the two child UserInput components in the same row. They collectively take up the same
width as a single UserInput component.
Props:
children: Exactly two UserInput components should be added as children.
*/
function DoubleUserInput({ children }) {
const userInputs = Children.toArray(children);
// Define the additional styling props for each of the children components
const leftInputProps = {
mainContainerProps: { style: styles.smallLeftMainContainer },
inputContainerProps: { style: styles.smallInputContainer },
};
const rightInputProps = {
mainContainerProps: { style: styles.smallRightMainContainer },
inputContainerProps: { style: styles.smallInputContainer },
};
return (
<View style={styles.doubleInputContainer}>
<View style={styles.leftContainer}>{cloneElement(userInputs[0], { ...leftInputProps })}</View>
<View style={styles.rightContainer}>
{cloneElement(userInputs[1], { ...rightInputProps })}
</View>
</View>
);
}
DoubleUserInput.propTypes = {
// eslint-disable-next-line react/require-default-props
children: (props, propName, componentName) => {
const prop = props[propName];
let error = null;
if (!prop.length || prop.length !== 2) {
error = new Error(`\`${componentName}\` should have exactly two children`);
} else {
Children.forEach(prop, (child) => {
if (child.type !== UserInput) {
error = new Error(`\`${componentName}\` children should be of type \`UserInput\`.`);
}
});
}
return error;
},
};
The issue is, if I include less than two children then I get an error when I try to render the children in the userInputs array (since one or both are undefined). Why isn't the type checking occurring first and giving me a warning? Is this expected (I mean I guess that I don't want it to work in this case but I would have expected the type-checking error)? Is there a better way I should be specifying that I need two child components? I'm pretty new to React and javascript so till trying to wrap my head around some of the conventions e.t.c.

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.

React how to test props passed to component passed as prop

Okay, this might sound complicated but it will be easy if you read the following example. The main purpose of this is to separate the logic from the actual render code. Making the component smaller and (in theory) easier to test.
class NameProvider {
public getName(): Promise<string> {
return Promise.resolve("Cool name");
}
}
interface RenderProps {
name: string;
onGetNamePress(): void;
}
interface LogicProps {
nameProvider: NameProvider;
render: React.ComponentType<RenderProps>
}
function Render({name, onGetNamePress}: RenderProps): React.ReactElement {
return <>
<p>{name}</p>
<button title="Get name!" onClick={onGetNamePress} />
</>
}
function Logic({nameProvider, render: Render}: LogicProps): React.ReactElement {
const [name, setName] = React.useState<string>();
return <Render
name={name}
onGetNamePress={fetch}
/>
async function fetch() {
setName(await nameProvider.getName());
}
}
Testing the render component is rather easy, but how do I test that the props passed to the render component are correct? Especially after the state changed.
Consider the following:
it('fetches the name after the button was pressed', () => {
const mnp = new MockNameProvider();
render(<Logic
nameProvider={mnp}
render={({name, onGetNamePress}) => {
act(async () => {
await onGetNamePress();
expect(name).toBe(mockName);
})
}}
/>)
})
This will cause an infinite loop, as the state keeps getting changed and the name fetched. I also couldn't imagine how to get the new props. This current code will test the old ones to my understanding. So my question is, how do I test if the props are correctly passed (also after updates).
(Important) Notes:
I'm actually writing a react native app, so maybe the issue is specific to native testing but I didn't think so.
This is not code from our codebase and just cobbled together. Thus also the React prefix, vscode just liked that better in an unsaved file.

React cloneElement not passing properties

In one of my projects I am trying to use a single child of my component as a "template" to render a set of products. I am cloning the child like so
useEffect(() => {
if (!children) {
return;
}
setTemplate(React.Children.only(children));
}, [children, setTemplate]);
useEffect(() => {
if (loading || !products || !template) {
return;
}
const rc = [];
products.forEach((p, i) => {
rc.push(
React.cloneElement(template, {
product: p,
key: i,
})
);
});
setRenderedChildren(rc);
}, [products, loading, template, setRenderedChildren]);
When I render this, the clones are created, however the properties never arrive to the underlying component.
Can anyone tell me what I am doing wrong?
It turns out, that the child component had some bugs and hence the properties defined in cloneElement are never assigned to my expected Component.

React wrapper: React does not recognize the `staticContext` prop on a DOM element

I'm trying to create a wrapper component around the react-router-dom NavLink component.
I would like my custom component to accept all of NavLinks props, and proxy them down to NavLink.
However when I do this, I'm getting:
Warning: React does not recognize the staticContext prop on a DOM
element. If you intentionally want it to appear in the DOM as a custom
attribute, spell it as lowercase staticcontext instead. If you
accidentally passed it from a parent component, remove it from the DOM
element.
A working demo of the issue can be found here:
https://codesandbox.io/s/w0n49rw7kw
There is a way to overcome that is using:
const { to, staticContext, ...rest } = this.props;
So your ...rest will never contain staticContext
This is a common problem with a simple solution as documented in the React documentation:
The unknown-prop warning will fire if you attempt to render a DOM
element with a prop that is not recognized by React as a legal DOM
attribute/property. You should ensure that your DOM elements do not
have spurious props floating around.
The spread operator can be used to pull variables off props, and put
the remaining props into a variable.
function MyDiv(props) {
const { layout, ...rest } = props
if (layout === 'horizontal') {
return <div {...rest} style={getHorizontalStyle()} />
} else {
return <div {...rest} style={getVerticalStyle()} />
}
}
You can also assign the props to a new object and delete the keys that
you’re using from the new object. Be sure not to delete the props from
the original this.props object, since that object should be considered
immutable.
function MyDiv(props) {
const divProps = Object.assign({}, props);
delete divProps.layout;
if (props.layout === 'horizontal') {
return <div {...divProps} style={getHorizontalStyle()} />
} else {
return <div {...divProps} style={getVerticalStyle()} />
}
}
This happens because you probably used {...props} somewhere in your component.
Example from React:
function MyDiv(props) {
const { layout, ...rest } = props
if (layout === 'horizontal') {
return <div {...rest} style={getHorizontalStyle()} />
} else {
return <div {...rest} style={getVerticalStyle()} />
}
}
We grab layout separately so that it won't be contained in {...rest}.
The given answer by the React docs was not quite good enough for my situation, so I found/developed one which isn't perfect, but is at least not so much of a hassle.
You can see the Q/A in which it arose here:
What is Reacts function for checking if a property applies?
The gist is, use a function to pick the bad props out for you.
const SPECIAL_PROPS = [
"key",
"children",
"dangerouslySetInnerHTML",
];
const defaultTester = document.createElement("div")
function filterBadProps(props: any, tester: HTMLElement = defaultTester) {
if(process.env.NODE_ENV !== 'development') { return props; }
// filter out any keys which don't exist in reacts special props, or the tester.
const out: any = {};
Object.keys(props).filter((propName) =>
(propName in tester) || (propName.toLowerCase() in tester) || SPECIAL_PROPS.includes(propName)
).forEach((key) => out[key] = props[key]);
return out;
}
Personally, I felt that the warning was completely useless in the first place, so I added a line which skips the check entirely when not in development mode (and warnings are suppressed). If you feel that the warnings have merit, just remove the line:
if(process.env.NODE_ENV !== 'development') { return props; }
You can use it like this:
public render() {
const tooManyProps = this.props;
const justTheRightPropsForDiv = filterBadProps(tooManyProps);
const justTheRightPropsForSpan = filterBadProps(tooManyProps, document.createElement("span"));
return (<div {...justTheRightPropsForDiv}>
<span {...justTheRightPropsForSpan} />
</div>)
}
If someone has this issue with react-admin, check if you don't have a Link as a child of Admin. Like this:
<Admin layout={props => <Layout/>}>
<Link to="/something">something</Link> <-- causing issue
</Admin>
Just move it to another component. For instance, inside the Layout.
I got the same issue when passing data in child component with camelCase property.
Warning: React does not recognize the moreInfo prop on a DOM element.
If you intentionally want it to appear in the DOM as a custom attribute,
spell it as lowercase moreinfo instead. If you accidentally passed it
from a parent component, remove it from the DOM element.
<CenteredModal
moreInfo={viewType}
/>
To fix that error, I used all lowercase letters for property.
<CenteredModal
moreinfo={viewType}
/>

Resources