Passing props to specific nested children in React - javascript

I am trying to configure some props to children.
In this dummy example I am testing the function in order to specifically target any child, nested or not, with the following prop : swipeMe.
It works very well if inside my div on the render function if it contains just a single child, like this:
<SwapableItems>
<div>
{/*the p element will get the red color as defined on childrenHandler*/}
<p swipeMe>Add Props</p>
</div>
</SwapableItems>
Yet, if I add more children into my div, somehow I guess that my ternary operation on childrenHandler is not working well and I do not know why...
If it has children, clone this element and call childrenHandler passing its children.
If it has my desired prop, clone de element and do something with it.
If none of the above, just clone the element.
return childHasChildren
? React.cloneElement(child, {}, childHasChildren)
: child.props.swipeMe
? React.cloneElement(child, { ...swipeMeProps })
: React.cloneElement(child, {});
Below is the full script.
You can also check it out on Codesandbox
import React from "react";
import ReactDOM from "react-dom";
function App() {
return (
<SwapableItems>
<div>
<p swipeMe>Add Props</p>
<div>Don't Add Props</div>
</div>
</SwapableItems>
);
}
function SwapableItems({ children }) {
const content = childrenHandler(children, { style: { color: "red" } });
return content;
}
const childrenHandler = (children, swipeMeProps) => {
const childEls = React.Children.toArray(children).map((child) => {
const childHasChildren =
child.props.children && React.isValidElement(child.props.children)
? childrenHandler(child.props.children, swipeMeProps)
: undefined;
return childHasChildren
? React.cloneElement(child, {}, childHasChildren)
: child.props.swipeMe
? React.cloneElement(child, { ...swipeMeProps })
: React.cloneElement(child, {});
});
return childEls;
};
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);

I tested this work.
I wrapped nested children by SwapableItems if has children.
function SwapableItems({ children }) {
const props = { style: { color: "red" } };
return Children.map(children, (child) => {
let nestedChild = child.props.children;
const hasNestedChild = nestedChild && typeof nestedChild !== "string"
if (hasNestedChild) {
nestedChild = <SwapableItems>{nestedChild}</SwapableItems>;
}
return child.props?.swipeMe || hasNestedChild
? cloneElement(child, child.props?.swipeMe ? props : {}, [nestedChild])
: child;
});
}
https://codesandbox.io/s/kind-snow-8zm6u?file=/src/App.js:345-751

child.props.children is an array, so React.isValidElement(child.props.children) is always falsy, to fix it try using React.cloneElement on it:
React.isValidElement(React.cloneElement(child.props.children)); // true
A style reset example:
const styleA = {
color: "blue"
};
const InlineReset = ({ children }) => {
return React.Children.map(children, child => {
console.log(child.props);
return React.cloneElement(child.props.children, { style: null });
});
};
export default function App() {
return (
<InlineReset>
<div>
<h1 style={styleA}>Hello </h1>
</div>
</InlineReset>
);
}

This is the solution:
I have initialized a variable which will hold the recursion outside of the logic and undefined.
let childHasChildren;
I have encapsulated the code inside an if statement with some adaptions:
"in case child has children, either an array or object, and if within the passed children there is/are valid React elements"
const deeperChildren = child.props.children;
const arrayOfChildren = Array.isArray(deeperChildren);
const singleChildren =
typeof deeperChildren === "object" && deeperChildren !== null;
if (
(arrayOfChildren &&
deeperChildren.some((c) => React.isValidElement(c))) ||
(singleChildren && React.isValidElement(deeperChildren))
) {
In order to pass the recursion without errors, in case the code within the if statement is called, I have cloned a filter/single object of which children would be React valid elements, then I pass just these/this into the recursion:
const validChildren = arrayOfChildren
? deeperChildren.filter((c) => React.isValidElement(c))
: deeperChildren;
Here is the result
Now it accepts, one item with children, or an Array. This can be configured in order to dynamically pass props, with default ones and other props that could be passed from outside of the component, although the latter is not my case. This solution was desired in order to achieve more complex stuff without using those solutions, such as render props, props contracts, HOCs, etc.
const childrenHandler = (children, swipeMeProps) => {
const childEls = React.Children.toArray(children).map((child) => {
let childHasChildren;
const deeperChildren = child.props.children;
const arrayOfChildren = Array.isArray(deeperChildren);
const singleChildren =
typeof deeperChildren === "object" && deeperChildren !== null;
if (
(arrayOfChildren &&
deeperChildren.some((c) => React.isValidElement(c))) ||
(singleChildren && React.isValidElement(deeperChildren))
) {
const validChildren = arrayOfChildren
? deeperChildren.filter((c) => React.isValidElement(c))
: deeperChildren;
childHasChildren = childrenHandler(validChildren, swipeMeProps);
}
return childHasChildren
? React.cloneElement(child, {}, childHasChildren)
: child.props.swipeMe
? React.cloneElement(child, { ...swipeMeProps })
: React.cloneElement(child, {});
});
return childEls;
};

Related

React: Render children only if condition is true

I'm new in React. I'm developing a screen but I have a issue, I don't know how insert the children in the parent if the state condition is equals, I'm using an array to print the parent and children but depends of the data the parent could have a children or not, for example if (parent.rework_name === children.rework_name) ? print the children : 'nothing in the parent'.
Please let me know if you have an idea how to solve this, many many thanks in advance.
This is the goal, my code works but the damn children is outside the parent :(
class Filling extends Component {
constructor() {
super();
this.state = {
fillingStations: [],
};
}
componentDidMount() {
getDataAPI('http://localhost:8080/api/parent')
.then((station) => {
getDataAPI('http://localhost:8080/api/children')
.then((data) => {
const stationArray = [];
station.map((item, index) => {
stationArray.push(
<ReworkStation key={index} title={index + 1} status='' />,
);
data.map((it, idx) => {
const f2Date = it.f2_time.substr(0, 10);
const f2Hour = it.f2_time.substr(11, 8);
const f2DateFormatted = `${f2Date.substr(8, 2)}/${f2Date.substr(5, 2)}/${f2Date.substr(0, 4)}`;
const color = selection_color(it.color_d);
return (
stationArray.push(item.rework_name === it.rework_name && <ReworkTitle key={idx} vin={it.vin} date={f2DateFormatted} ipsq={it.defects} hour={f2Hour} color={color} />)
);
});
});
console.log(stationArray);
this.setState({
fillingStations: stationArray,
});
});
});
}
render() {
return (
<div className='row'>
{ this.state.fillingStations }
</div>
);
}
}
I don't know how to insert the children inside the parent already render.
I already solved, first render all the parent divs and after replace the position array with array.splice
render() {
const array = [];
this.state.fillingStations.map((item, index) => (
array.push(<Parent key={index} title={index + 1} status='' />),
this.state.fillingChildren.map((it, ind) => {
if (item.name === it.name) {
parent.splice(index, 1,
<Parent {...this.props}}>
<Child {...this.props} />
</Parent >);
}
})
));
return (
<div className='row'>
{array}
</div>
);
}
}

React Compound Components type warning

I have a simple compound component with a bunch of static subcomponents:
// #flow
import React, { Component, Children } from 'react';
type Props = {
children: React.ChildrenArray<React.Node> | React.Node,
}
class Toggle extends Component<Props> {
static On = props => (props.on ? props.children : null);
static Off = props => (props.on ? null : props.children);
static Button = props => (
<button
onClick={props.toggle}
type="button"
style={{ display: 'inline-block' }}
>
<pre>{JSON.stringify(props.on, null, 2)}</pre>
</button>
);
state = { on: false }
toggle = () => {
this.setState(
({ on }) => ({ on: !on }),
// maybe this.props.someCallback
() => console.log(this.state.on),
);
}
render() {
return Children.map(
this.props.children,
childElem => React.cloneElement(childElem, {
on: this.state.on,
toggle: this.toggle,
}),
);
}
}
export default Toggle;
The warning happens when I try to put some other elements into Toggle children scope.
For example:
<Toggle>
<Toggle.On>On</Toggle.On>
<span /> <-- this is savage
<Toggle.Button />
<Toggle.Off>Off</Toggle.Off>
</Toggle>
Everything is working, but my flowtype warn me about this span like so:
Warning: Received `false` for a non-boolean attribute `on`.....
Warning: Invalid value for prop `toggle` on <span> tag....
How can I to pacify this nasty girl?
Thank you guys, I think, right solution is just check if type of mounted node is correct one, otherwise - just clone node with regular node props:
// #flow
import React, { Component, Children } from 'react';
type Props = {
children: React.ChildrenArray<React.Node> | React.Node,
}
class Toggle extends Component<Props> {
static On = props => (props.on ? props.children : null);
static Off = props => (props.on ? null : props.children);
static Button = props => (
<button
onClick={props.toggle}
type="button"
style={{ display: 'inline-block' }}
>
<pre>{JSON.stringify(props.on, null, 2)}</pre>
</button>
);
state = { on: false }
toggle = () => {
this.setState(
({ on }) => ({ on: !on }),
// maybe this.props.someCallback
() => console.log(this.state.on),
);
}
// Checking here
allowedTypes = ({ type }) => {
return [
(<Toggle.On />).type,
(<Toggle.Off />).type,
(<Toggle.Button />).type,
].includes(type);
}
render() {
return Children.map(
this.props.children,
(childElem) => {
const elemProps = this.allowedTypes(childElem) ? {
on: this.state.on,
toggle: this.toggle,
} : childElem.props;
return React.cloneElement(childElem, elemProps);
},
);
}
}
export default Toggle;
You can also do this, just having the components in a list and checking their type inside .map, putting on the custom props or otherwise just returning the original child.
const allowedTypes = [ToggleOn, ToggleOff, ToggleButton]
return React.Children.map(props.children, child => {
if (allowedTypes.includes(child.type)) {
return React.cloneElement(child, {on, toggle})
}
return child
})
}

how to prevent repetitive shuffle of my array using onClick

I need your fresh eyes to help me.
I have a set of answers in my array which I shuffle on the first render.
My problem here, is that I know if i am clicking on one of the answer, the setState will re-render and consequently re-shuffle my array which i dont want.
You can have a look at my code below:
export default class extends React.Component {
constructor(props) {
super(props)
this.state = {
user: this.props.user,
token: this.props.token,
data: this.props.data,
count: 0,
select: undefined
}
this.changeQuestion = this.changeQuestion.bind(this);
this.onCorrect = this.onCorrect.bind(this);
this.onFalse = this.onFalse.bind(this);
}
static async getInitialProps({req, query}) {
const id = query.id;
const authProps = await getAuthProps(req, 'Country/Questions?theory=' + id)
return authProps
}
componentDidMount() {
if (this.state.user === undefined) {
Router.push('/login')
}
}
changeQuestion() {
this.setState({
count: this.state.count + 1,
select: undefined
})
}
onCorrect() {
this.setState({
select: true
})
}
onFalse() {
this.setState({
select: true
})
}
mixAnswers() {
const answer = this.props.data.Properties.Elements
const answers = answer[this.state.count].Properties.Answers
const answersObj = answers.reduce((ac, el, i) => {
ac.push(
<p key={i} onClick={i === 0
? this.onCorrect
: this.onFalse} className={i === 0
? 'exercices__answers--correct'
: 'exercices__answers--false'}>{el}</p>
)
return ac
}, [])
const answersShuffled = answersObj.sort(() => 0.5 - Math.random())
return answersShuffled;
}
render() {
const {user, token, data} = this.state
const answer = this.props.data.Properties.Elements
const answers = answer[this.state.count].Properties.Answers
return (
<div>
{user !== undefined
? <Layout user={this.state.user}>
<div>
{answer[this.state.count].Properties.Sources !== undefined
? <img src={answer[this.state.count].Properties.Sources[0].URL}/>
: ''}
<h1>{answer[this.state.count].Properties.Question}</h1>
{this.mixAnswers().map((el, i) => <p key={i} onClick={el.props.onClick} className={this.state.select !== undefined
? el.props.className
: ''}>{el.props.children}</p>)
}
<p>{answer[this.state.count].Properties.Description}</p>
</div>
<button onClick={this.changeQuestion}>Next Question</button>
</Layout>
: <h1>Loading...</h1>}
</div>
)
}
}
Obviously, the way I am using the 'this.mixAnswers()' method is the issue. How can I prevent it to re-render then re-shuffle this array of questions.
PS: dont pay attention about onCorrect() and onFalse().
You should make sure the logic that shuffle the answers is called only once, you can get this behavior on ComponentWillMount or ComponentDidMount, then you save them in the state of the component and in the render function instead of
{this.mixAnswers().map((el, i) => <p key={i} onClick={el.props.onClick} className={this.state.select !== undefined
? el.props.className
: ''}>{el.props.children}</p>)
}
You use this.state.answers.map()...

Passing props to component via variable

I have a class component which renders an input and a tooltip and controls state. The <SelectInputType> components job is to take a single prop of 'type' and render either a text input, a textarea, a select input or a checkbox group component. There are a load of props that need to be passed through this SelectInputType component, some of which are relevant to all 4 of the input components (placeholderText and required for example) and some of which are specific to a particular input (options for example is only relevant to checkboxes and select inputs).
render() {
const inputProps = {};
inputProps.placeholderText = this.props.placeholderText;
inputProps.required = this.props.required;
inputProps.class = this.props.class;
inputProps.value = this.props.value;
inputProps.options = this.props.options;
inputProps.updateText = this.handleInput;
inputProps.handleFocus = this.focusIn;
inputProps.handleBlur = this.focusOut;
inputProps.handleCheckboxChange = e => this.addCheckboxToList(e);
inputProps.closeCheckboxSelector = this.closeCheckboxSelector;
inputProps.readableSelectedCheckboxes = this.state.readableSelectedCheckboxes;
const inputClass = classNames('input-with-tooltip', {
focus: this.state.focus,
'step-complete': this.state.completed,
});
return (
<div className={inputClass}>
<InputTooltip
tooltipText={this.props.tooltipText}
completed={this.state.completed}
/>
<div className="drill-creation-input">
<SelectInputType type={this.props.type} {...inputProps} />
</div>
</div>
);
}
My SelectInputType component looks like this...
const SelectInputType = (props) => {
let component;
if (props.type === 'title') {
component = <TextInput />;
} else if (props.type === 'text') {
component = <TextareaInput />;
} else if (props.type === 'checkbox') {
component = <CheckboxInput />;
} else if (props.type === 'select') {
component = <SelectInput />;
}
return (
<div>
{component}
// Need to pass props through to this?
</div>
);
};
I am using the JSX spread attribute to pass the props down to the SelectInputType component, but I don't know how to then pass these on to the 4 input components (and if I do pass them on will I have errors with certain props not being valid on particular components?)
You could alternatively save the component type in the variable, not the component itself:
const SelectInputType = (props) => {
let ComponentType;
if (props.type === 'title') {
ComponentType = TextInput;
} else if (props.type === 'text') {
ComponentType = TextareaInput;
} else if (props.type === 'checkbox') {
ComponentType = CheckboxInput;
} else if (props.type === 'select') {
ComponentType = SelectInput;
}
return (
<div>
<ComponentType {..props} />
</div>
);
};
You might get errors. If so you could create utility functions to extract the props you need per input type:
extractTextInputProps(props) {
const { value, className, required } = props
return { value, className, required }
}
extractSelectInputProps(props) {
const { value, handleBlur, updateText } = props
return { value, handleBlur, updateText }
}
You could probably extract a more generic function out of this so you don't have to repeat the property name twice.
Then use them when creating your components with the spread operator:
let component;
if (props.type === 'title') {
component = <TextInput { ...extractTextInputProps(props) } />;
} else if (props.type === 'text') {
component = <TextareaInput />;
} else if (props.type === 'checkbox') {
component = <CheckboxInput />;
} else if (props.type === 'select') {
component = <SelectInput { ...extractSelectInputProps(props) } />;
}

How to add a prop to a specific element by cloning the element in React?

I am trying to add a prop to a specific React component, not to all of the component's children.
The function that add the prop looks like the following code:
addProps (children) {
const childProps = { doSomething: this.doSomething };
const recursiveAddProps = (children) => {
return React.Children.map(children, child => {
if (child.type !== GrandChild){
if (child.props) recursiveAddProps(child.props.children);
return child;
}
return React.cloneElement(child, childProps);
});
}
return recursiveAddProps(children);
}
Example
But that is not adding the function doSomething to the GrandChild component, so no onClick event is fired when the component is clicked on.
What would you do? Thanks for your help!
You're not cloning the intermediate children and ignoring the return value of recursiveAddProps. cloneElement doesn't mutate so you need to do it like this (untested):
const recursiveAddProps = (children) => {
return React.Children.map(children, child => {
if (child.type !== GrandChild) {
if (child.props) return React.cloneElement(child, {
children: recursiveAddProps(child.props.children),
});
return child;
}
return React.cloneElement(child, childProps);
});
}

Categories