Google Map nodes do not deal well with being removed from the DOM and everything following is trying to deal with that.
Up until now I've created a map node within a React component at the highest level in my app and never unmounted it. I'm using a bunch of convoluted css to show/hide/position this node based on what route I'm on and also using a lot of logic in the compenentWillReceiveUpdate() lifecycle method. I really want to use a Google Map within normal mounted/unmounted components and have it nested in the DOM tree where I intend it to be.
I'm playing with placing a <div id='my-google-map> node on <body> and pulling it into a component in componentDidMount() and placing it back on <body> when unmounting. Here's what I'm currently working with.
componentDidMount: function () {
const { mapWrapper } = this.refs
let mapNode = document.getElementById('my-google-map')
if (!mapNode) {
mapNode = document.createElement('div')
mapNode.setAttribute('id', 'my-google-map')
}
mapWrapper.appendChild(mapNode)
this.setState({
mapNode: mapNode
})
},
componentWillUnmount: function () {
document.body.appendChild(this.state.mapNode)
},
render: function () {
return (
<div ref='mapWrapper'>
<span>{this.props.currentDropdown}</span>
</div>
)
}
This seems to be fine and re-renders look like they just ignore the appended node but this feels weird and I'm worried I'm missing something.
Will/when will a future render delete the appended node? Should I just return false from shouldComponentUpdate() to prevent that?
Is there a better way to have a persistent non-React node within React?
Related
Given the code below, my child component alerts trigger before any of the code in the Parent mounted function.
As a result it appears the child has already finished initialization before the data is ready and therefor won't display the data until it is reloaded.
The data itself comes back fine from the API as the raw JSON displays inside the v-card in the layout.
My question is how can I make sure the data requested in the Parent is ready BEFORE the child component loads? Anything I have found focuses on static data passed in using props, but it seems this completely fails when the data must be fetched first.
Inside the mounted() of the Parent I have this code which is retrieves the data.
const promisesArray = [this.loadPrivate(),this.loadPublic()]
await Promise.all(promisesArray).then(() => {
console.log('DATA ...') // fires after the log in Notes component
this.checkAttendanceForPreviousTwoWeeks().then(()=>{
this.getCurrentParticipants().then((results) => {
this.currentP = results
this.notesArr = this.notes // see getter below
})
The getter that retrieves the data in the parent
get notes() {
const newNotes = eventsModule.getNotes
return newNotes
}
My component in the parent template:
<v-card light elevation="">
{{ notes }} // Raw JSON displays correctly here
// Passing the dynamic data to the component via prop
<Notes v-if="notes.length" :notesArr="notes"/>
</v-card>
The Child component:
...
// Pickingn up prop passed to child
#Prop({ type: Array, required: true })
notesArr!: object[]
constructor()
{
super();
alert(`Notes : ${this.notesArr}`) // nothing here
this.getNotes(this.notesArr)
}
async getNotes(eventNotes){
// THIS ALERT FIRES BEFORE PROMISES IN PARENT ARE COMPLETED
alert(`Notes.getNotes CALL.. ${eventNotes}`) // eventNotes = undefined
this.eventChanges = await eventNotes.map(note => {
return {
eventInfo: {
name: note.name,
group: note.groupNo || null,
date: note.displayDate,
},
note: note.noteToPresenter
}
})
}
...
I am relatively new to Vue so forgive me if I am overlooking something basic. I have been trying to fix it for a couple of days now and can't figure it out so any help is much appreciated!
If you are new to Vue, I really recommend reading the entire documentation of it and the tools you are using - vue-class-component (which is Vue plugin adding API for declaring Vue components as classes)
Caveats of Class Component - Always use lifecycle hooks instead of constructor
So instead of using constructor() you should move your code to created() lifecycle hook
This should be enough to fix your code in this case BUT only because the usage of the Notes component is guarded by v-if="notes.length" in the Parent - the component will get created only after notes is not empty array
This is not enough in many cases!
created() lifecycle hook (and data() function/hook) is executed only once for each component. The code inside is one time initialization. So when/if parent component changes the content of notesArr prop (sometimes in the future), the eventChanges will not get updated. Even if you know that parent will never update the prop, note that for performance reasons Vue tend to reuse existing component instances when possible when rendering lists with v-for or switching between components of the same type with v-if/v-else - instead of destroying existing and creating new components, Vue just updates the props. App suddenly looks broken for no reason...
This is a mistake many unexperienced users do. You can find many questions here on SO like "my component is not reactive" or "how to force my component re-render" with many answers suggesting using :key hack or using a watcher ....which sometimes work but is almost always much more complicated then the right solution
Right solution is to write your components (if you can - sometimes it is not possible) as pure components (article is for React but the principles still apply). Very important tool for achieving this in Vue are computed propeties
So instead of introducing eventChanges data property (which might or might not be reactive - this is not clear from your code), you should make it computed property which is using notesArr prop directly:
get eventChanges() {
return this.notesArr.map(note => {
return {
eventInfo: {
name: note.name,
group: note.groupNo || null,
date: note.displayDate,
},
note: note.noteToPresenter
}
})
}
Now whenever notesArr prop is changed by the parent, eventChanges is updated and the component will re-render
Notes:
You are overusing async. Your getNotes function does not execute any asynchronous code so just remove it.
also do not mix async and then - it is confusing
Either:
const promisesArray = [this.loadPrivate(),this.loadPublic()]
await Promise.all(promisesArray)
await this.checkAttendanceForPreviousTwoWeeks()
const results = await this.getCurrentParticipants()
this.currentP = results
this.notesArr = this.notes
or:
const promisesArray = [this.loadPrivate(),this.loadPublic()]
Promise.all(promisesArray)
.then(() => this.checkAttendanceForPreviousTwoWeeks())
.then(() => this.getCurrentParticipants())
.then((results) => {
this.currentP = results
this.notesArr = this.notes
})
Great learning resource
I have a JavaScript function that returns a DOM node that represents a tree view with many nested nodes inside it, I would like to know if it is possible to pass that function to vue render function ??
I know I should not be messing with the DOM or maybe instead use a recursive template, but for the sake of learning and challenge myself, I would still like to figure this out... how to manipulate the DOM within Vue, with the help of render functions.
Thanks for any help.
I'm not aware of any mechanism to do this (at least not officially). The render function is not the place to manipulate the DOM in any way.
You can still manipulate the DOM carefully outside of the render function without bothering Vue too much. If you have a blank placeholder <div> in your template with a ref, then appending a child to it should be fine, as long as the DOM element remains in place (using v-if on the div may interfere, but I'm not 100% certain of this). Vue usually does a good job of reusing the same DOM element across re-renders – but that's the thing, you're messing with the DOM which Vue thinks it has complete control over, and you aren't guaranteed Vue will cooperate with any external changes to the DOM that it didn't expect.
Thinking about this a bit more, you can achieve what you want with a custom Vue directive or component. Something like this:
Vue.directive('el', {
bind(el, binding) {
if (binding.value) {
el.appendChild(binding.value)
}
},
update(el, binding) {
// Check if bound element changed
if (binding.oldValue !== binding.newValue) {
if (binding.oldValue) {
// Remove old element
binding.oldValue.remove()
}
if (binding.value) {
// Append new element
el.appendChild(binding.value)
}
}
}
})
// Create a DOM element manually
const btn = document.createElement('button')
btn.textContent = 'Click'
new Vue({
el: '#app',
render(h) {
return h('div', {
directives: [
{
name: 'el',
value: btn
}
]
})
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app"></div>
We have a crazy DOM hierarchy, and we've been passing JSX in props rather than embedding children. We want the base class to manage which documents of children are shown, and which children are docked or affixed to the top of their associated document's window.
List (crazy physics writes inline styles to base class wrappers)
Custom Form (passes rows of JSX to Base class)
Base Class (connects to list)
Custom Form (passes rows of JSX to base class)
Base class (connects to list)
The problem is that we're passing deeply nested JSX, and state management / accessing refs in the form is a nightmare.
I don't want to re-declare every row each time, because those rows have additional state attached to them in the Base Class, and the Base Class needs to know which rows actually changed. This is pretty easy if I don't redeclare the rows.
I don't know how to actually deal with rows of JSX in Custom Form.
Refs can only be appended in a subroutine of render(). What if CustomForm wants to measure a JSX element or write inline CSS? How could that JSX element exist in CustomForm.state, but also have a ref? I could cloneElement and keep a virtual DOM (with refs) inside of CustomForm, or depend on the base class to feed the deeply-nested, mounted ref back.
I believe it's bad practice to write component state from existing state. If CustomForm state changes, and I want to change which rows are passed to BaseClass, I have to throttle with shouldComponentUpdate, re-declare that stage document (maintaining row object references), then call setState on the overarching collection. this.state.stages.content[3].jsx is the only thing that changed, but I have to iterate through every row in every stage document in BaseClass when it sees that props.stages changed.
Is there some trick to dealing with collections of JSX? Am I doing something wrong? This all seems overly-complicated, and I would rather not worsen the problem by following some anti-pattern.
Custom Form:
render () {
return <BaseClass stages={this.stages()}/>
}
stages () {
if (!this._stages) this._stages = { title: this.title(), content: this.content() };
return this._stages;
}
title () {
return [{
canBeDocked: false,
jsx: (
<div>A title document row</div>
)
}
}
content () {
return [{
canBeDocked: false,
jsx: (
<div>Hello World</div>
)
}, {
canBeDocked: true,
jsx: (
<div>Yay</div>
)
}
}
What I usually do is just connect the lower level components via Redux. This helps with not passing the state in huge chunks from the top-most component.
A great video course by one of the React creators, Dan Abramov: Getting started with Redux
Absolutely agree with #t1gor. The answer for us was to use REDUX. It changed the entire game for us. Suddenly a button that is nested 10 levels deep (that is, inside a main view, header, header-container, left side grid, etc, etc, deeper and deeper) into purely custom components, has a chance to grab state whenever it needs.
Instead of...
Parent (pass down state) - owns state vars
Child (will pass down again) - parent has state vars
Grandchild (will pass down a third time) - grandparent has state vars
Great Grandchild (needs that state var) - great grandparent has state vars
You can do...
Parent (no passing) - reads global state vars
Child
Grandchild
Great Grandchild - also reads same global level state vars without being passed...
Usually the code looks something like this...
'use strict'
//Importation of Connection Tools & View
import { connect } from 'react-redux';
import AppView from './AppView';
//Mapping -----------------------------------
const mapStateToProps = state => {
return {
someStateVar: state.something.capturedInState,
};
}
const mapDispatchToProps = dispatch => {
return {
customFunctionsYouCreate: () => {
//do something!
//In your view component, access this by calling this.props.customFunctionsYouCreate
},
};
}
//Send Mappings to View...
export default connect(mapStateToProps, mapDispatchToProps)(AppView);
Long story short, you can keep all global app state level items in something called a store and whenever even the tiniest component needs something from app state, it can get it as the view is being built instead of passing.
The issue is having content as follows, and for some reason not being able to effectively persist the child instances that haven't changed (without re-writing the entire templateForChild).
constructor (props) {
super(props);
// --- can't include refs --->
// --- not subroutine of render --->
this.state = {
templateForChild: [
<SomeComponentInstance className='hello' />,
<AnotherComponentInstance className='world' />,
],
};
}
componentDidMount () {
this.setState({
templateForChild: [ <div className='sometimes' /> ],
}); // no refs for additional managing in this class
}
render () {
return ( <OtherManagerComponent content={this.state.templateForChild} /> );
}
I believe the answer could be to include a ref callback function, rather than a string, as mentioned by Dan Abramov, though I'm not yet sure if React does still throw a warning. This would ensure that both CustomForm and BaseClass are assigned the same ref instance (when props.ref callback is executed)
The answer is to probably use a key or createFragment. An unrelated article that addresses a re-mounting problem. Not sure if the fragment still includes the same instances, but the article does read that way. This is likely a purpose of key, as opposed to ref, which is for finding a DOM node (albeit findDOMNode(ref) if !(ref instanceof HTMLElement).
My application has lots of different pages (composite components?) which contain the various discrete components that make up the page. I'd like to be able to run a static function on each of the discrete components, but I only have a reference to the one composite page.
Is there a way I can render the page on the server side and maintain some sort of reference to each component that was included in the page (recursively)? I'm imagining an analogy similar to the Node.js require graph, which is built when a module is compiled.
I know I can do this at the top level using refs, but this wouldn't work, because I will eventually need refs of refs.
This fiddle is a basic model of what my components look like. However, in my project I don't even have refs on the top level.
var Goodbye = React.createClass({
statics: {
serverSideMethod: function() {
console.log('ya, server');
}
},
render: function() {
return <div className="goodbye">Goodbye {this.props.name}</div>;
}
});
var Hello = React.createClass({
componentDidMount: function(){
console.log('will mount', this.refs);
},
render: function() {
return (
<div>
Hello {this.props.name}
<Goodbye name="cruel world" ref="g1"/>
<Goodbye name="cruel world" ref="g2"/>
</div>
)
}
});
React.render(<Hello name="World"/>,document.getElementById('container'));
https://jsfiddle.net/69z2wepo/12471/
In reality, I only have a reference to the very top level component through react-router. So ideally, I want to be able to recursively call the static on each child node. However, there are no child nodes, because they are not passed into the render, but rather included by the page.
Here's some more context of what this looks like in my express router
and my related unanswered question.
I admit that I might be going about this wrong, so assuming that I have a static that I need to fire on each child component in the page (recursively) any ideas on a better approach?
My ultimate goal is to collect all the private css strings I have defined in the modules and key them up in an object and write them to the head before I render the whole page. Thanks.
I've recently started using ReactJS in my UI projects which has greatly simplified my UI workflow. Really enjoyable API to work with.
I've noticed recently that I've had to use a pattern in a couple of my projects that needed to aggregate data on a page. This data would live in the DOM and not be dependent on using the React state for data transitions.
This is an example implementation:
var Component = module.exports = React.createClass({
componentDidMount: function() {
this.component = new Component();
this.component.start();
},
componentWillUnmount: function(prevProps, prevState) {
if (this.component !== undefined) {
this.component.destroy();
}
},
render: function() {
return (
<div id="componentContainer"></div>
);
}
});
var Component = function(){
// Some object that dynamically loads content in a
// pre-packaged NON-react component into componentContainer
// such as a library that renders a graph, or a data loader
// that aggregates DOM elements using an infinite scroll
}
My question is whether or not this is the proper way to aggregate data into the DOM using React. I looked around for the idiomatic way of doing this, but my google-foo was unable to come up with anything.
Thanks!
EDIT - as a side note, does anyone think there will be a problem with the way I destroy the container, using the componentWillUnmount?
The main problem is that you're using an id, which is inflexible and makes assumptions about the rest of the components (because ids must be globally unique).
module.exports = React.createClass({
componentDidMount: function() {
// pass a DOM node to the constructor instead of it using an id
this.component = new Component(this.getDOMNode());
this.component.start();
},
componentWillUnmount: function() {
this.component.destroy();
},
render: function() {
return <div />;
}
});
Your componentWillUnmount was fine, but the one place you set this.component will always run before componentWillUnmount, and there's no other reason it'd be assigned/deleted, so the if statement isn't needed.
Also the arguments both weren't used, and aren't provided to componentWillUnmount. That signature belongs to componentDidUpdate.