React.js Component children to DOM element - javascript

I am trying to create my own Map component and one of the tasks is to create custom Map Annotation. This is how it looks like a MapAnnotation component in the render function
<MapAnnotation title='Annotation' latitude={ 12.345678 } longitude={ 12.345678 }>
<div style={{width: '100px', height: '100px', backgroundColor: 'lightblue'}} />
</MapAnnotation>
The method that converts MapAnnotation component into the map object is pretty simple:
createAnnotation(child) {
const { latitude, longitude, title } = child.props
var coordinate = new global.mapkit.Coordinate(latitude, longitude)
if ('children' in child.props) {
var newAnnotation = new global.mapkit.Annotation(coordinate, function(coordinate, options) {
var domElement =
return ReactDOMServer.renderToString(<div>{child.props.children}</div>)
}, { title: title })
return newAnnotation
} else {
var newAnnotation = new global.mapkit.MarkerAnnotation(coordinate, { title: title })
return newAnnotation
}
}
Unfortunately, I do not know how to return the DOM element from children that exist in MapAnnotation. Right now I am getting the error:
Error: [MapKit] Annotation element factory must return a DOM element,
got <div data-reactroot=""><div style="width:100px;height:100px;background-color:lightblue"></div></div>
instead.
I am the React.js beginner and most of my knowledge I have from ReactNative. This is the reason why I do not know what is the keyword to find the solution with Uncle Google.
I will be glad for help :)

The solution based on Boykov answer:
var domElement = document.createElement("div")
domElement.innerHTML = ReactDOMServer.renderToString(child.props.children)
return domElement

I think I can do it using useRef.
import react,{useRef, useEffect} from 'react';
const Test = ({book_id, chart_id, title, data_source}) => {
const refCom = useRef(null);
useEffect(() => {
if (refCom && refCom?.current) {
//Run the code you want to do inside.
console.log(refCom.current);
}
}, [refCom]);
return (<div ref={refCom}>....</div>)

Related

React: Generating dynamically a nested component

I am really struggling in this problem.
I am making an admin page based with next.js, I am working for the layouts.
What I want is based of the url path, I generate the nested layouts for my web page.
For example: if the URL is home/test/route1 then I should generate in a variable something like this:
<HomeLayout>
<TestLayout>
<Route1Layout>
<main>{children}</main>
</Route1Layout>
</HomeLayout>
</TestLayout>
This is how I have implement it in the main component file (layoutsManager.js):
// we import the layout that will be persistent trought the home url and his child
import HomeLayout from "./home/HomeLayout.jsx";
// this map must containt other map or Component functions
const layoutsMap = new Map(Object.entries({
"home": new Map(Object.entries({
"index":HomeLayout,
"test": new Map(Object.entries({
"route1":HomeLayout,
"route2":HomeLayout
}))
})),
}));
export default function DisplayLayout({urlPath,children}) {
let pathArray = urlPath.slice(1).split("/");
function GenerateLayout({path, map, children}) {
let newMap = map;
let layoutsArray = [];
path.forEach((name) => {
if (newMap.has(name)) {
if (newMap.get(name) instanceof Map) {
newMap = newMap.get(name);
if (newMap.has("index")) {
let NewElement = newMap.get("index");
layoutsArray.push(NewElement);
}
}
else {
let NewElement = newMap.get(name);
layoutsArray.push(NewElement);
}
}
});
let NewLayout = ({children}) => <>{children}</>;
if (layoutsArray.length > 0) {
let LastLayout = layoutsArray.pop();
layoutsArray.forEach((Layout,index) => {
NewLayout = <NewLayout><Layout></Layout></NewLayout>;
});
NewLayout = <NewLayout><LastLayout></LastLayout></NewLayout>;
}
else {
NewLayout = <h1 className="text-red-600 font-bold text-2xl">Error, there is no layouts in the layoutsMap Map</h1>;
}
return (NewLayout);
};
return (
<GenerateLayout path={pathArray} map={layoutsMap}>{children}</GenerateLayout>
);
};
To explain it, in the local GenerateLayout component, I first retrieve all the React Component from the layoutsMap variable then I get an array full of react components.
Next I read each element of the array by adding the child component into the new parent component and when I reach the last element, I add the last child with the content of the page.
Like this for the following array [HomeLayout,TestLayout,Route1Layout]:
Index 0:
<><HomeLayout></HomeLayout></>
Index 1:
<><HomeLayout><TestLayout></TestLayout></HomeLayout></>
Index 2:
<><HomeLayout><TestLayout><Route1Layout><main>{children}</main></Route1Layout></TestLayout></HomeLayout></>
I hope you get the idea but my nightmare is that I always get stuck with the same damn error:
Element type is invalid: expected a string (for built-in components) or a class/function (for composite components) but got: ...
Any help would be really appreciated.
Thanks in advance.

Using React Hooks, when I pass down a prop from parent to a child component, the prop in the child component is undefined

What am I trying to do?
I'm trying to set an array of objects in which the value within the array is dependent on the parent component.
What is the code that currently tries to do that?
Here are the different files simplified:
// Parent.
export default function Parent() {
const [filePaths, setFilePaths] = useState();
useEffect(() => {
var fileContent = JSON.parse(fs.readFileSync("./config.json"); // Reading from a JSON.
var tempFilePaths = [];
fileContent.FilePaths.forEach((file) => {
tempFilePaths.push(file);
});
setFilePaths(tempFilePaths); // Contents of "config.js" is now in the "useState".
}, []);
return (
<Child filePaths={filePaths}/>
)
}
// Child.
export default function Child({filePaths}) {
var links = [
{
path: filePaths[0].Link1,
},
{
path: filePaths[0].Link2,
},
]
return (
<div>Nothing here yet, but I would map those links to front-end links.</div>
)
}
// config.json
{
"url": "http:localhost:3000",
"FilePaths": [
{
"Link1": "C:\Documents\Something",
"Link2": "C:\Documents\SomethingElse"
}
]
}
When I render the "filePaths" in the return() of the Child component, the "filePaths" is able to be rendered, but I wish to set the "filePaths" to the variable "links".
What do I expect the result to be?
I expect the variable "links" to be fine in the child component, being able to be used within the child component.
What is the actual result?
When starting the app I get a TypeError: Cannot read property '0' of undefined.
What I think the problem could be?
I think the child component renders without the parent component finishing the useEffect(). I'm wondering if there's a way to tell the child component to wait for the parent component to finish, then proceed with setting the variable of "links".
filePaths will be undefined because you call useState() with empty input.
There are two options (you can choose one) to solve this:
Initialize filePaths inside the useState()
Return the Child component if the filePaths is not null/undefined.
export default function Parent() {
const [filePaths, setFilePaths] = useState();
useEffect(() => {
var fileContent = JSON.parse(fs.readFileSync("./config.json"); // Reading from a JSON.
var tempFilePaths = [];
fileContent.FilePaths.forEach((file) => {
tempFilePaths.push(file);
});
setFilePaths(tempFilePaths); // Contents of "config.js" is now in the "useState".
}, []);
return (
// return the Child component if the filePaths is not null/undefined
{filePaths && <Child filePaths={filePaths}/>}
)
}
I personally prefer the second one because we can add a loading component when the filePaths is still null/undefined.
You are right, that's why you should change your child component. It renders the filePaths whether it is defined or not.
Try to do as follows.
export default function Child({filePaths}) {
const [links, setLinks] = useState(filePaths);
useEffect(()=>{
setLinks(filePaths);
},[filePaths])
return (
<div>Nothing here yet, but I would map those links to front-end links.</div>
)
}
I think you're right on your guess about the sequence of methods calling:
According to this, when you use useEffect the method gets called after the rendering, as if it was a componentDidMount lifecycle method, which is supported by the React official lifecycle diagram and React Documentation. And that is the reason because the props.filePaths whithin the Child component is undefined.
To avoid this, you should try to set an initial value (in the useState method).
something like the following (maybe extracting the repetition as a function):
// Parent.
export default function Parent() {
var fileContent = JSON.parse(fs.readFileSync("./config.json"); // Reading from a JSON.
var tempFilePaths = [];
fileContent.FilePaths.forEach((file) => {
tempFilePaths.push(file);
});
const [filePaths, setFilePaths] = useState(tempFilePaths);
useEffect(() => {
var fileContent = JSON.parse(fs.readFileSync("./config.json"); // Reading from a JSON.
var tempFilePaths = [];
fileContent.FilePaths.forEach((file) => {
tempFilePaths.push(file);
});
setFilePaths(tempFilePaths); // Contents of "config.js" is now in the "useState".
}, []);
return (
<Child filePaths={filePaths}/>
)
}
const [filePaths, setFilePaths] = useState();will initialize filePaths as undefined.
so you can check firstly like
if (!filePaths) return null in the parent;
or set initState in useState like
#see https://reactjs.org/docs/hooks-reference.html#lazy-initial-state
const [filePaths, setFilePaths] = useState(()=> {
var fileContent = JSON.parse(fs.readFileSync("./config.json");
var tempFilePaths = [];
fileContent.FilePaths.forEach((file) => {
tempFilePaths.push(file);
});
return tempFilePaths;
});

Need Help Simplifying an Immutable.js Array of Objects update

I'm new to immutablejs and have managed to update a property of an object stored in an array of objects. My goal is to simplify my development but I feel like I'v made it more complicated. Clearly, I'm missing something to make this simpler.
I created to stackblitz projects, one with the code without immutablejs https://stackblitz.com/edit/react-before-immutable , and one with immutablejs https://stackblitz.com/edit/react-after-immutable
(code below also).
I've seen some examples here where people use the second parameter of findIndex, but that function never got called for me. It also is not in the docs so I'm guessing it's not supported any more.
With Immutable.js
import React, { useState } from 'react';
import { List } from 'immutable';
export default () => {
const init = [{
id: 101,
interestLevel: 1
},
{
id: 102,
interestLevel: 0
}];
const [myArray, setMyArray] = useState(init);
const updateRow = (e) => {
const id = parseInt(e.target.attributes['data-id'].value);
const immutableMyArray = List(myArray);
const index = List(myArray).findIndex((item) => {
return item.id === id;
});
const myRow = immutableMyArray.get(index);
myRow.interestLevel++;
const newArray = immutableMyArray.set(index, myRow);
setMyArray(newArray);
};
return (
<ul>
{
myArray.map(function (val) {
return (
<li key={val.id}>
<button onClick={updateRow} data-id={val.id} >Update Title</button>
{val.id} : {val.interestLevel}
</li>
)
})
}
</ul>
)
}
Without Immutable.js
import React, { useState } from 'react';
export default () => {
const init = [{
id: 101,
interestLevel: 1
},
{
id: 102,
interestLevel: 0
}];
const [myArray, setMyArray] = useState(init);
const updateRow = (e) => {
const id = e.target.attributes['data-id'].value;
const newMyArray = [...myArray];
var index = newMyArray.findIndex(a=>a.id==id);
newMyArray[index].interestLevel = myArray[index].interestLevel + 1;
setMyArray(newMyArray);
}
return (
<ul>
{
myArray.map(function (val) {
return (
<li key={val.id}>
<button onClick={updateRow} data-id={val.id} >Update Title</button>
{val.id} : {val.interestLevel}
</li>
)
})
}
</ul>
)
}
Have you considered the purpose of immutablejs?
In your example, you are only adding needless complexity to your code, without leveraging the gains provided by the library.
The purpose of immutable is to provide immutable collections, inspired from scala. In other words, you create your collection, then you pass it to another component, and you can be certain that no element was appended or removed. The individual elements, however, are not under such guarantee, partly due to the constraints (or lack thereof) brought by JS.
As it stands in your code, there are very few reasons to do something like this. I've taken the liberty of changing your quote quite a bit in order to showcase how to do so:
class Comp extends React.Component {
constructor(props) {
super(constructor);
if (props.interests) {
this.state = {
interests: props.interests
}
} else {
this.state = {
interests: Immutable.Set([])
}
}
}
updateRow(e) {
return function() {
this.setState({
interests: this.state.interests.update((elements) => {
for (var element of elements) {
if (element.id == e) {
element.interestLevel++;
}
}
return elements;
})
});
}
}
render() {
var interests = this.state.interests;
var updateRow = this.updateRow;
var list = this;
//return (<div>Test</div>);
return ( <
ul > {
interests.map(function(val) {
return ( <
li key = {
val.id
} >
<
button onClick = {
updateRow(val.id).bind(list)
}
data-id = {
val.id
} > Update Title < /button> {
val.id
}: {
val.interestLevel
} <
/li>
)
})
} <
/ul>
)
}
}
var interests = Immutable.Set([{
id: 1,
interestLevel: 0
},
{
id: 2,
interestLevel: 0
}
])
ReactDOM.render( < Comp interests = {
interests
}
/>, document.getElementById("app"));
<script src="https://cdn.jsdelivr.net/npm/immutable#4.0.0-rc.12/dist/immutable.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.7.0/umd/react.development.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.7.0/umd/react-dom.production.min.js"></script>
<div id='app'></div>
The changes:
Rewrote your component as a class. This is purely to highlight the rest of the changes
Your component now takes an external prop containing interests. This means you can pass a Set and be sure that it won't have elements added out of the blue within the component
The component is in charge of the interest levels. As such, whenever you click on one of the buttons, the update function is called on the Set, which is used to update the items inside the collection
The entire thing is rendered as an array through render()
This is both more readable and easier to manage.
As you mentioned in this comment, you're looking for easier ways of updating objects in a deep object hierarchy using Immutable.js.
updateIn should do the trick.
const immutableObject = Immutable.fromJS({ outerProp: { innerCount: 1 } });
immutableObject.updateIn(['outerProp', 'innerCount'], count => count + 1);
It's also worth noting that you probably want to call Immutable.fromJS() instead of using Immutable.List() since the latter won't deeply convert your JavaScript object into an Immutable one, which can lead to bugs if you're assuming the data structure to be deeply Immutable. Switching the code above to use Immutable.fromJS() and updateIn we get:
// In updateRow
const immutableMyArray = fromJS(myArray);
const index = immutableMyArray.findIndex((item) => {
return item.id === id;
});
const newArray = immutableMyArray.updateIn([index, 'interestLevel'], interestLevel => interestLevel + 1);
setMyArray(newArray);

Possible to render react component within mapboxgl.Popup() in .setHTML or .setDOMContent?

I am wondering if it is possible to render a react component within a mapboxgl.Popup(). Something like this:
componentDidMount() {
new mapboxgl.Popup()
.setLngLat(coordinates)
.setHTML(`<div>${<MapPopup />}<p>${moreText}</p></div>`)
//.setDOMContent(`${<MapPopup />}`) ?????
.addTo(this.props.mapboxMap)
})
Or should this be done using ReactDOM.render?
ReactDOM.render(<MapPopup />, document.getElementById('root'))
This project will have buttons and inputs in the popup that connect to a redux store.
Thanks for any input!
This works:
addPopup(el: JSX.Element, lat: number, lng: number) {
const placeholder = document.createElement('div');
ReactDOM.render(el, placeholder);
const marker = new MapboxGl.Popup()
.setDOMContent(placeholder)
.setLngLat({lng: lng, lat: lat})
.addTo(map);
}
(Where I've used typescript to illustrate types, but you can just leave these out for pure js.) Use it as
addPopup(<h1>Losers of 1966 World Cup</h1>, 52.5, 13.4);
You can try to implement React component:
export const Popup = ({ children, latitude, longitude, ...mapboxPopupProps }) => {
// this is a mapbox map instance, you can pass it via props
const { map } = useContext(MapboxContext);
const popupRef = useRef();
useEffect(() => {
const popup = new MapboxPopup(mapboxPopupProps)
.setLngLat([longitude, latitude])
.setDOMContent(popupRef.current)
.addTo(map);
return popup.remove;
}, [children, mapboxPopupProps, longitude, latitude]);
return (
/**
* This component has to have 2 divs.
* Because if you remove outter div, React has some difficulties
* with unmounting this component.
* Also `display: none` is solving that map does not jump when hovering
* ¯\_(ツ)_/¯
*/
<div style={{ display: 'none' }}>
<div ref={popupRef}>
{children}
</div>
</div>
);
};
After some testing, I have realized that Popup component was not rendering properly on the map. And also unmounting the component was unsuccessful. That is why there are two divs in return. However, it may happen only in my environment.
See https://docs.mapbox.com/mapbox-gl-js/api/#popup for additional mapboxPopupProps
useEffect dependencies make sure that MapboxPopup gets re-created every time something of that list changes & cleaning up the previous popup instance with return popup.remove;
I've been battling with this as well. One solution I found was using ReactDOM.render(). I created an empty popup then use the container generated by mapboxgl to render my React component.
marker.setPopup(new mapboxgl.Popup({ offset: 18 }).setHTML(''));
markerEl.addEventListener('mouseenter', () => {
markerEl.classList.add('enlarge');
if (!marker.getPopup().isOpen()) {
marker.getPopup().addTo(this.getMap());
ReactDOM.render(
component,
document.querySelector('.mapboxgl-popup-content')
);
}
});
const mapCardNode = document.createElement("div");
mapCardNode.className = "css-class-name";
ReactDOM.render(
<YourReactPopupComponent / > ,
mapCardNode
);
//if you have a popup then we remove it from the map
if (popupMarker.current) popupMarker.current.remove();
popupBox.current = new mapboxgl.Popup({
closeOnClick: false,
anchor: "center",
maxWidth: "240px",
})
.setLngLat(coordinates)
.setDOMContent(mapCardNode)
.addTo(map);
I used MapBox GL's map and popup events (to improve upon #Jan Dockal solution) which seemed to improve reliability. Also, removed the extra div wrapper.
import { useWorldMap as useMap } from 'hooks/useWorldMap'
import mapboxgl from 'mapbox-gl'
import { FC, useRef, useEffect } from 'react'
export const Popup: FC<{
layerId: string
}> = ({ layerId, children }) => {
const map = useMap() // Uses React Context to get a mapboxgl map (could possibly be null)
const containerRef = useRef<HTMLDivElement>(null)
const popupRef = useRef<mapboxgl.Popup>()
const handleClick = (
e: mapboxgl.MapMouseEvent & {
features?: mapboxgl.MapboxGeoJSONFeature[] | undefined
} & mapboxgl.EventData
) => {
// Bail early if there is no map or container
if (!map || !containerRef.current) {
return
}
// Remove the previous popup if it exists (useful to prevent multiple popups)
if (popupRef.current) {
popupRef.current.remove()
popupRef.current = undefined
}
// Create the popup and add it to the world map
const popup = new mapboxgl.Popup()
.setLngLat(e.lngLat) // could also use the coordinates from a feature geometry if the source is in geojson format
.setDOMContent(containerRef.current)
.addTo(map)
// Keep track of the current popup
popupRef.current = popup
// Remove the tracked popup with the popup is closed
popup.on('close', () => {
popupRef.current = undefined
})
}
useEffect(() => {
if (map && layerId) {
// Listen for clicks on the specified layer
map?.on('click', layerId, handleClick)
// Clean up the event listener
return () => {
map?.off('click', layerId, handleClick)
popupRef.current?.remove()
popupRef.current = undefined
}
}
}, [map, layerId])
return <div ref={containerRef}>{children}</div>
}
Try to do with onClick event, instead of creating a button. After that put your react component in onClick events add event listener refrence link
[1]: https://stackoverflow.com/a/64182029/15570982

React Native: renderRow not executed

In the following code, when setState is called from campaignsUpdated, render gets logged to the console, but not renderRow:
var React = require('react-native'),
Bus = require('../Bus'),
styles = require('../Styles'),
CampaignsStore = require('../stores/Campaigns'),
CampaignItem = require('./CampaignItem'),
{
Component,
Text,
TextInput,
ListView,
View,
NavigatorIOS,
ActivityIndicatorIOS
} = React
class CampaignList extends Component {
constructor(props) {
super(props)
this.state = {
dataSource: new ListView.DataSource({rowHasChanged: (r1, r2) => r1 !== r2})
}
}
componentDidMount() {
this.addListeners()
Bus.emit('campaigns:search', '')
}
componentWillUnmount() {
this.removeListeners()
}
render() {
console.log('render')
return (
<View style={styles.container}>
<TextInput
style={styles.searchInput}
placeholder='Campaign Name'
value={this.state.campaignName}
onChange={this.campaignSearchChanged.bind(this)}/>
<ListView
dataSource = {this.state.dataSource}
renderRow = {this.renderRow.bind(this)}/>
</View>
)
}
renderRow(campaign) {
console.log('renderRow')
return <CampaignItem campaign={campaign}/>
}
addListeners() {
Bus.on({
'campaigns:updated': this.campaignsUpdated.bind(this)
})
}
removeListeners() {
Bus.off({
'campaigns:updated': this.campaignsUpdated.bind(this)
})
}
campaignsUpdated(event) {
var campaigns = event.data
this.setState({
dataSource: this.state.dataSource.cloneWithRows(campaigns)
})
}
campaignSearchChanged(event) {
var campaignName = event.nativeEvent.text
Bus.emit('campaigns:search', campaignName)
}
}
module.exports = CampaignList
What am I doing wrong here?
You are passing ListView a function renderRow that returns a component. You would have to call that function within ListView once it is passed, presumably during a map over campaigns.
By the looks of it the most likely case is that you have a classic React mutability issue here.
I.e. I suspect your 'campaignsUpdated' method is called with either the same Array instance it received last time, or the elements within the list are the same instances.
Try using:
campaignsUpdated(event) {
var campaigns = event.data.slice(); // <-- clone the array
this.setState({
dataSource: this.state.dataSource.cloneWithRows(campaigns)
})
}
If that doesn't work, then you either make the part that manages your list of compaigns create new copies when changes are made (e.g. const clone = {...campaign, title:"A new Title"}) or update your rowHasChanged method to see if the title (or whatever data you need) has actually changed.
Here are two really good videos about immutability in JavaScript here:
https://egghead.io/lessons/javascript-redux-avoiding-array-mutations-with-concat-slice-and-spread
https://egghead.io/lessons/javascript-redux-avoiding-object-mutations-with-object-assign-and-spread

Categories

Resources