Parse react component map to new component - reactjs

I am trying to figure out how to take a react component as input and map to a new component tree (replacing tags and modifying as desired). In this specific case, I would like to take a component written for DOM output and modify it to for native output.
So a very simplified component might go from:
<MyComponent>
<p>foo</p>
</MyComponent>
to:
<MyComponent>
<Text>foo</Text>
</MyComponent>
Bonus points for modifying individual tags props, such as adding style props, event handlers, et cetera. I know if there is an answer it won't be one-size-fits-all. I'm just hoping for some guidelines as to general approach/others who have encountered this use-case and pushed it forward.

Something like this seems to work reasonably well, although passing styles down the tree is an open question. Well, at least we know that the transpilation from one set of tags to another is possible. Props https://stackoverflow.com/users/2578335/giles-copp
// traverse.js
import React from 'react';
export function kindOf(node) {
if (node === null || node === undefined || typeof node === 'boolean') {
return 'Empty';
}
if (typeof node === 'string' || typeof node === 'number') {
return 'Text';
}
if (Array.isArray(node)) {
return 'Fragment';
}
const { type } = node;
if (typeof type === 'string') {
return 'DOMElement';
}
return 'ComponentElement';
}
export function defaultTraverse(path) {
const kind = kindOf(path.node);
if (kind === 'Empty') {
return path.node;
}
if (kind === 'Text') {
return path.node;
}
if (kind === 'Fragment') {
return path.node.map(path.traverse);
}
return React.cloneElement(
path.node,
path.node.props,
...path.traverseChildren(),
);
}
export default function traverse(node, visitor) {
const {
Empty = defaultTraverse,
Text = defaultTraverse,
Fragment = defaultTraverse,
DOMElement = defaultTraverse,
ComponentElement = defaultTraverse
} = visitor;
const path = {
node,
kindOf,
defaultTraverse() {
return defaultTraverse(path);
},
traverse(childNode, childVisitor = visitor) {
return traverse(childNode, childVisitor);
},
traverseChildren(childVisitor = visitor) {
return React.Children.toArray(path.node.props.children).map(
(childNode) => path.traverse(childNode, childVisitor)
);
},
visitor
};
if (node === null || node === undefined || typeof node === 'boolean') {
return Empty(path);
}
if (typeof node === 'string' || typeof node === 'number') {
return Text(path);
}
if (Array.isArray(node)) {
return Fragment(path);
}
const { type } = node;
if (typeof type === 'string') {
return DOMElement(path);
}
return ComponentElement(path);
}
// transpile.js
import React from 'react';
import { Image, Text, View } from 'react-native';
import traverse from './traverse';
const viewNodeTypes = ['div'];
const textNodeTypes = ['h1', 'p'];
const inlineNodeTypes = ['span', 'b', 'em'];
const dom2ReactNative = (node) => traverse(node, {
ComponentElement(path) {
if (path.node.type === 'img') {
return React.createElement(
Image,
path.node.props
);
}
},
DOMElement(path) {
if (textNodeTypes.includes(path.node.type)) {
return React.createElement(
Text,
path.node.props,
...path.traverseChildren(),
);
} else if (viewNodeTypes.includes(path.node.type)) {
return React.createElement(
View,
path.node.props,
...path.traverseChildren(),
);
} else if (inlineNodeTypes.includes(path.node.type)) {
return path.node.props.children;
}
return React.cloneElement(
path.node,
path.node.props,
...path.traverseChildren(),
);
}
});
export default dom2ReactNative;

Related

React button component asks for key props

I've been coding a flexible button component where I pass some values by param (text, class, size, icon, and such) and when I do a condition (if it's a link or a button) before returning the HTML, it asks me for a key prop.
Why is it asking for a key prop if it's not a list. I'm not even going through any array to return the value.
This is the React button component code:
import React from "react";
import "./button.css";
import { Icon } from '../Icon/icon';
const buttonTypes = [
"button",
"a"
];
const buttonClasses = [
"app-button",
"app-button-filled",
"app-button-outlined",
"app-button-icon"
];
const buttonSizes = [
"app-button-large",
"app-button-icon-large"
];
export const Button = ({
buttonIcon = {
name: '',
style: '',
position: ''
},
buttonText,
buttonType,
buttonTarget,
buttonHref,
buttonOnClick,
buttonClass,
buttonSize
}) => {
const checkClasses = () => {
if(buttonClasses.includes(buttonClass)){
return buttonClasses[0]+" "+buttonClass;
} else {
return buttonClasses[0];
}
}
const checkSizes = () => {
if(buttonSizes.includes(buttonSize)){
return buttonSize;
} else {
return '';
}
}
const checkTypes = () => {
if(buttonTypes.includes(buttonType)){
return buttonType;
} else {
return buttonTypes[0];
}
}
const insertContent = () => {
let content = [],
iconTag = <Icon iconName={buttonIcon.name} iconStyle={buttonIcon.style} iconClass="app-button-svg" />;
if(buttonClass === "app-button-icon"){
content.push(iconTag);
} else {
if(buttonText){ content.push(<span className="app-button-text">{buttonText}</span>); }
if(buttonIcon){
if(buttonIcon.position === "left"){
content.unshift(iconTag);
} else if(buttonIcon.position === "right" || buttonIcon.position !== "left") {
content.push(iconTag);
}
}
}
return content;
}
if(checkTypes() === "button"){
return (<button className={`${checkClasses()} ${checkSizes()}`} onClick={buttonOnClick}>{insertContent()}</button>);
} else if(checkTypes() === "a"){
return (<a className={`${checkClasses()} ${checkSizes()}`} href={buttonHref} target={buttonTarget} >{insertContent()}</a>);
}
}
Warning code:
Warning: Each child in a list should have a unique "key" prop | button.js:84
The condition for this trigger is passing an array in the list of children.
You are doing this near the end:
return (<button className={`${checkClasses()} ${checkSizes()}`} onClick={buttonOnClick}>{insertContent()}</button>);
...
return (<a className={`${checkClasses()} ${checkSizes()}`} href={buttonHref} target={buttonTarget} >{insertContent()}</a>);
Your children here are the result of insertContent(), which returns an array. Because it returns an array, it triggers the warning because no keys are specified.
To solve this, you need to change the function insertContent to include keys on the elements it puts into the content array

I try to use useState but dont works weirdly

I used the useState of react to deal with "hooks" (is the name of it?)
const [users, setUsers] = useState(null);
In the next piece of code I use the setUsers but dosnt do it...
getUsersApi(queryString.stringify(params)).then(response => {
console.log(params.page)
// eslint-disable-next-line eqeqeq
if(params.page == "1"){
console.log(response)//line33
if(isEmpty(response)){
setUsers([]);
}else{
setUsers(response);
console.log(users);//line38
}
}else{
if(!response){
setBtnLoading(0);
}else{
setUsers({...users, ...response});
setBtnLoading(false);
}
}
})
I inserted a console.log inside of it and apparently pass through there, but dosnt set users...
Here is the function getUsersApi() in case you need it.
export function getUsersApi(paramsUrl){
console.log(paramsUrl)
const url = `${API_HOST}/users?${paramsUrl}`
const params = {
headers:{
Authorization: `Bearer${getTokenApi()}`,
},
};
return fetch(url, params).then(response => {
return response.json();
}).then(result => {
return result;
}).catch(err => {
return err;
});
}
Here is the function isEmpty() in case you need it.
function isEmpty(value) {
if (value == null) {
return true;
}
if (isArrayLike(value) &&
(isArray(value) || typeof value == 'string' || typeof value.splice == 'function' ||
isBuffer(value) || isTypedArray(value) || isArguments(value))) {
return !value.length;
}
var tag = getTag(value);
if (tag == mapTag || tag == setTag) {
return !value.size;
}
if (isPrototype(value)) {
return !baseKeys(value).length;
}
for (var key in value) {
if (hasOwnProperty.call(value, key)) {
return false;
}
}
return true;
}
Thanks a lot guys!!
If you want to know if the state has been updated, be sure to set a useEffect hook with the state inside the dependency array.
import React, { useEffect, useState } from 'react';
useEffect(() => {
console.log("Users: ", users)
}, [users])
If this gets logged, users have been sucessfully updated.
If this does NOT gets logged, you're not entering the else which calls setUsers

How to pass conditional props to unknown type of react component in typescript?

I have this components:
export function Hover(props: PolygonProps) {
//...
}
export function Route(props: CenterProps) {
//...
}
This enum:
export enum Highlight {
HOVER,
ROUTE,
}
This object:
export const HIGHLIGHTS = {
[Highlight.HOVER]: Hover,
[Highlight.ROUTE]: Route,
};
And this code in render:
let HighlightComponent;
сonst center = props.center;
const points = props.points;
if (highlight !== undefined) {
HighlightComponent = HIGHLIGHTS[highlight] as React.ComponentType<CenterProps | PolygonProps>;
}
const centerProps: CenterProps = { center };
const polygonProps: PolygonProps = { points };
return <div>{HighlightComponent && <HighlightComponent />}</div>
Question: how to pass props of needed type (CenterProps or PolygonProps) if i dont know type?
Is it correct structure for the case or not?
Since you have two seperate props, you must still check to see which one to use. So I would drop the HIGHLIGHTS constant and simply use a switch:
const centerProps: CenterProps = { center: 'someValue' };
const polygonProps: PolygonProps = { points: 'someOtherValue' };
let highlightComponent;
if (highlight !== undefined) {
switch (highlight) {
case Highlight.HOVER:
highlightComponent = <Hover {...polygonProps} />;
break;
case Highlight.ROUTE:
highlightComponent = <Route {...centerProps} />;
break;
}
}
return <div>{highlightComponent}</div>
Another option would be to define the props like this:
const centerProps: CenterProps = { center: 'someValue' };
const polygonProps: PolygonProps = { points: 'someOtherValue' };
export const HIGHLIGHT_PROPS = {
[Highlight.HOVER]: polygonProps,
[Highlight.ROUTE]: centerProps,
};
And then:
let HighlightComponent;
let highlightProps;
if (highlight !== undefined) {
HighlightComponent = HIGHLIGHTS[highlight] as React.ComponentType<CenterProps | PolygonProps>;
highlightProps = HIGHLIGHT_PROPS[highlight] as CenterProps | PolygonProps;
}
return <div>{HighlightComponent && <HighlightComponent {...highlightProps} />}</div>
If I understood correctly you can simply do:
if (highlight !== undefined) {
HighlightComponent = HIGHLIGHTS[highlight] === HIGHLIGHTS.HOVER
? <Hover {...polygonProps}/>
: <Route {...centerProps}/>;
}
return <div>{HighlightComponent && <HighlightComponent />}</div>
However this looks like a code smell so I would suggest that you change your props to be HighlightComponent: JSX.Element and just pass in the relevant component.

HOC to add prop if it's a certain type

I was trying to write a HOC that adds a prop if it's a certain type. I am iterating through it depth first. But when I try to set prop it says it's not extensible, I am trying to add prop of value to HEEHAW:
function fieldLayoutHOC(WrappedComponent: ComponentType) {
return (
class FieldLayoutWrap extends WrappedComponent {
static displayName = wrapDisplayName(WrappedComponent, 'FieldLayoutWrap')
render() {
const view = super.render()
// depth first - stack - last in first out
// iterate depth first until a Field is found
const elements = [view]; // stack
console.log('view:', view);
while (elements.length) {
const element = elements.pop();
const primative = typeof element;
if (primative === 'object') {
if (element.type === Field) {
// fields.push(element);
element.props.value = 'HEEHAW'; /////// not extensible error here
console.log('added value HEEHAWW');
} else {
if (element.props) {
const children = element.props.children;
if (children) {
if (Array.isArray(children)) {
elements.push(...children);
} else {
elements.push(children);
}
}
}
}
}
}
return view;
}
}
)
}
Am I doing it wrong?
Well I came up with my own solution. I'm not mutating the props, which added the complication of holding on to a mutable version of the tree. This can definitely use some cleaning.
function addPropsIfHOCFactory(predicate) { //
return function addPropsIfHOC(WrappedComponent) { // factory
return (
class FieldLayoutWrap extends WrappedComponent {
render() {
const view = super.render();
if (!this.addProps) return view;
// depth first - stack - last in first out
// iterate depth first until a Field is found
const viewElementNew = { node: view, parentElement: null };
const tree = [viewElementNew]; // stack // parentElement is ref to parentElement in elements
const elementsByDepth = {}; // key is depth, value is array of element's at that depth
const elementsByParentId = {}; // key is elementId of parent
let elementId = 0;
// console.log('view:', view);
let depthMax = 0;
while (tree.length) {
const element = tree.pop();
element.props = element.node.props ? element.node.props : undefined;
element.childrenElements = undefined;
element.clone = undefined;
element.depth = getDepth(element);
element.id = elementId++;
element.needsClone = false; // if true then clone, its set to true if props are changed
if (element.depth > depthMax) depthMax = element.depth;
if (!elementsByDepth[element.depth]) {
elementsByDepth[element.depth] = [];
}
elementsByDepth[element.depth].push(element);
const node = element.node;
const primative = typeof node;
if (primative === 'object' && node) {
if (predicate(node)) {
const addProps = isFunction(this.addProps) ? this.addProps(node) : this.addProps;
element.props = Object.assign({}, node.props ? node.props : undefined, addProps);
markBranchNeedsClone(element);
console.log('added props to node:', element.node);
}
}
if (node.props && node.props.children) {
const children = node.props.children;
const pushChild = child => {
const parent = element;
const childElement = {
node: child,
parentElement: parent
}
tree.push(childElement);
if (!elementsByParentId[parent.id]) elementsByParentId[parent.id] = [];
elementsByParentId[parent.id].push(childElement);
return childElement;
}
if (Array.isArray(children)) {
element.childrenElements = children.map(pushChild);
} else {
const child = children;
element.childrenElements = pushChild(child);
}
}
}
// do React.cloneElement from deepest first IF needsClone === true
let depth = depthMax + 1;
while (depth--) {
// console.log('doing now elementsByDepth[depth] of depth:', depth);
for (const element of elementsByDepth[depth]) {
if (typeof element.node === 'object' && element.node) {
if (!element.needsClone) {
element.clone = element.node;
} else {
let childrenClones = elementsByParentId[element.id];
if (childrenClones) {
if (childrenClones.length === 1) {
childrenClones = childrenClones[0].clone;
} else {
childrenClones = childrenClones.map(element => element.clone);
}
}
console.log('CLONING');
element.clone = React.cloneElement(element.node, element.props, childrenClones);
}
} else {
// its a string, number etc
element.clone = element.node;
}
}
}
// console.log('viewElementNew:', viewElementNew);
// console.log('elementsByDepth:', elementsByDepth);
// console.log('elementsByParentId:', elementsByParentId);
return viewElementNew.clone;
}
}
)
}
}
function isFunction(functionToCheck) {
var getType = {};
return functionToCheck && getType.toString.call(functionToCheck) === '[object Function]';
}
function getDepth(element) {
let depth = 0;
let elementCur = element;
while (elementCur.parentElement) {
elementCur = elementCur.parentElement;
depth++;
}
return depth;
}
function markBranchNeedsClone(element) {
let elementCur = element;
while (elementCur) {
elementCur.needsClone = true;
elementCur = elementCur.parentElement;
}
}
Usage:
import React from 'react'
import ReactDOM from 'react-dom'
import addPropsIfHOC from 'add-props-if'
// MY FORM COMPONENT
class BlahDumb extends React.Component {
addProps = {
placeholder: 'INJECTED PLACEHOLDER'
}
render() {
return (
<form>
<label htmlFor="url">URL</label>
<div>
<input id="url" type="text" />
yeppers
</div>
<div>
<input id="foo" type="text" />
</div>
</form>
)
}
}
const BlahPropsAdded = addPropsIfHOC(node => node.type === 'input')
const Blah = BlahPropsAdded(BlahDumb);
// MY APP COMPONENT
class App extends React.PureComponent {
render() {
return (
<div className="app">
<Blah />
</div>
)
}
}
// RENDER
ReactDOM.render(<App />, document.getElementById('app'))
Here it is working - https://codesandbox.io/s/6y1lrn7yww
Here is a demo which adds props to <Field> children: https://codesandbox.io/s/9zp9207nvy

React - how to determine if a specific child component exists?

If I have this structure:
const MyComponent = (props) => {
return (
<Wrapper />{props.children}</Wrapper>
);
}
and I use it like this:
<MyComponent>
<SomeInnerComponent />
</MyComponent>
How can I check to see if <SomeInnerComponent /> has specifically been included between <MyComponent></MyComponent>, from within the MyComponent function?
Given that you want to check that SomeInnerComponent is present as a child or not, you could do the following
const MyComponent = (props) => {
for (let child in props.children){
if (props.children[child].type.displayName === 'SomeInnerComponent'){
console.log("SomeInnerComponent is present as a child");
}
}
return (
<Wrapper />{props.children}</Wrapper>
);
}
Or you could have a propTypes validation on your component
MyComponent.propTypes: {
children: function (props, propName, componentName) {
var error;
var childProp = props[propName];
var flag = false;
React.Children.forEach(childProp, function (child) {
if (child.type.displayName === 'SomeInnerComponent') {
flag = true
}
});
if(flag === false) {
error = new Error(componentName + ' does not exist!'
);
}
return error;
}
},
Just want to provide an answer for a similar but different need, where you might have an HOC wrapping several layers of components, and you'd like to see if the HOC has already wrapped a component. The method I came up with was to have the HOC add a data-attribute onto the component to serve as a flag, which the HOC could then check on subsequent runs.
const WithFoo = Component = props => {
return props["data-with-foo"]
? <Component {...props} />
: (
<FooWrapper>
<Component {...props} data-with-foo />
</FooWrapper>
);
};
React nodes have a type property that's set to the constructor, or the function that created the node. So I wrote the following function for checking if a React node was created by a certain component.
const reactNodeIsOfType = (node, type) =>
typeof node === "object" && node !== null && node.type === type;
Usage:
const MyComponent = (props) => {
let hasInnerComponent = false;
React.Children.forEach(props.children, (child) => {
if (reactNodeIsOfType(child, SomeInnerComponent)) {
hasInnerComponent = true;
}
});
return (
<>
<div>hasInnerComponent: {hasInnerComponent ? "yes" : "no"}.</div>
<Wrapper>{props.children}</Wrapper>
</>
);
}
<MyComponent><div /></MyComponent>
// hasInnerComponent: no
<MyComponent><SomeInnerComponent /></MyComponent>
// hasInnerComponent: yes
const c = <SomeInnerComponent />;
console.log(reactNodeIsOfType(c, SomeInnerComponent));
// true
This is the TypeScript version. Calling it will also assign the correct type to the node's props.
const isObject = <T extends object>(value: unknown): value is T =>
typeof value === "object" && value !== null;
const reactNodeIsOfType = <
P extends object,
T extends { (props: P): React.ReactElement | null | undefined }
>(
node: React.ReactNode,
type: T
): node is { key: React.Key | null; type: T; props: Parameters<T>[0] } =>
isObject<React.ReactElement>(node) && node.type === type;

Resources