The React way to retrieve (some) DOM values - javascript

My react Menu component renders a list of links. Other parts need to animate (width and x/y pos) depending on the width of the Menu component's link's widths so all this needs to happen after all has rendered, and I need to read values from the DOM.
I'm looking for a proper way, no react anti-pattern, to handle this scenario but can't really sort it out in a way I'm happy with.
Here's an explanatory flow.
1) User clicks a link in the menu.
2) Activate that link.
this.setState({link: {active: true}});
3) Look up and store what width the clicked element has.
let w = this.myLink.offsetWidth; // myLink from refs.
4) Update state with new width
this.setState({styleState: { width: w }}); // Styles used in component elsewhere.
OK! This works fairly well, now I want to do the same for pos X/Y which makes things more complicated. Now I'll need to getBoundingClientRect() and take consideration to this element's parents to get the correct position etc.
I keep wishing for a sensible DOM API where I can grab values from the DOM (yep, jQuery style - like jqLite), then with values I update my state and thus re-render the component. I realize it wouldn't make sense to manipulate the DOM this way so I'm not trying to, but in this specific case the truth is actually in the DOM already and I need to extract it.
Is there a lib ppl use for these things, just to get a couple of getter methods like el.width(), el.left() for convenience?

I don't think its bad style to read size/styles of the dom after rendering. Just be sure to not modify the dom directly. The reason is that the real dom should always be in sync with the virtual dom.
I would encapsulate this behaviour in a custom Link component that receives an onClick:
//untested pseudocode
class Link extends Component {
handleRendered = (el) => {
this.width = el.clientWidth;
....
}
handleClick = (ev) => {
this.props.onClick({
width: this.width,
x: this.x,
...
})
}
render = () => {
return (
<div ref={(el) => this.handleRendered(el)} onClick={this.handleClick}>
{this.props.children}
</div>
);
}
}
class MyPage extends Component {
handleClick = ({width, ....}) => {
this.setState({
otherElementStyle: {
width: width
}
})
}
render = () => {
return (
<Link onClick={this.handleClick}>
Menu1
</Link>
);
}
}
Concerning your requested library, I'm not aware of any but I bet there is one.

Related

Plotly - Dash #app.callback does not re-render Dash elements inside custom component unless re-render is triggered somehow

Inside usage.py I try to update graph-tooltip:
#app.callback(
Output("grid-value", "children"),
Input("live-graph", "hoverData"),
)
def display_hover(hoverData):
if hoverData is None:
return no_update
# demo only shows the first point, but other points may also be available
pt = hoverData["points"][0]
x = pt["x"]
y = pt["y"]
children = f"{x}:{y}"
return children
Both the live-graph and grid-value elements are inside custom Dash component (dash-react-flow, but with a new node type that can contain anything passed to it from Dash).
Now if I over a the live-graph, it gets updated only once I click, so it seems not to be re-rendering properly.
I implemented the node:
function AnyContentNode({ data }) {
return (
<div className="any-content-node" id={data.id} >
<div>
{
data.elements?.map((node, index) => {
return window.elements[data.elementId][node];
})
}
</div>
{
data.handles?.map((node, index) => {
return <Handle key={index} type={node.direction} position={node.position} id={node.id} />
})
}
</div>
);
}
where window.elements contains the node that is not being updated properly. I suspect that storing elements inside window.elements might be the cause of this, but I did not find any other way to pass those elements into the custom node without having to change the source code for ReactFlow.
So far it works for non-dynamic elements, but currently I got stuck when I have to update dynamic element. It gets occasionally updated, but that is not very good user-experience.
Is there a way to force re-render/or somehow fix this? I am quite new to React, so I don't have a good knowledge of the default hooks etc. I tried to see whether componentDidUpdate gets triggered and it does (so many times, every second).

Reactjs: how to write a method to handle component creation and unmount

So let's say there is acomponent which displays 2 child components: a document list and the selected document. By default the selected document component is not rendered, only when a document is selected from the list. And i also want this whole thing work when a new document is selected from the list.
There is a state which holds the document content and responsible for the selected document rendering, so i thought i'm going to set it to null in the method which handles the list item selection in order to unmount the previously created child component. Like this (excerpts from the parent class):
handleResultListItemClick(docname) {
if (this.state.sectioncontainer != null) this.setState({sectioncontainer: null},()=>{console.log("muhoo");});
var selected_doc = this.state.resultlist.filter((doc) => {
return docname === doc.properties.title;
});
this.setState({sectioncontainer: selected_doc[0].content.sections},()=>{console.log("boohoo");});
}
...
render() {
return (
...
{this.state.sectioncontainer != null && <SectionContainer listOfSections={this.state.sectioncontainer}/>}
);
}
The only problem is that state handling is not fast enough (or something) in react, because putting the state nullification and its new value setting in the same method results in no change in ReactDOM.
With the above code, the component will be created when the parent component first rendered, but after selecting a new doc in the list results in no change.
How should i implement this in way which works and also elegant?
I found this: ReactDOM.unmountComponentAtNode(container) in the official react docs. Is this the only way? If yes, how could i get this container 'name'?
Edit:
Based on the answers and thinking the problem a bit more through, i have to explain more of the context.
As kingdaro explained, i understand why there is no need to unmount a child component on a basic level, but maybe my problem is bit more sophisticated. So why did i want to unmount the child?
The documents consist of several subsections, hence the document object which is passed to the child component is an array of objects. And the document is generated dynamically based on this array the following way (excerpt from the SectionContainer class which is responsible to display the document):
buildSectionContainer() {
return this.props.listOfSections.map((section, index) =>
{
if (section.type === 'editor') return (
<QuillEditor
key={index}
id={section.id}
modules={modules}
defaultValue={section.content}
placeholder={section.placeholder}
/>
);
else if (section.type === 'text') return (
<div key={index}>{section.value}</div>
);
}
);
}
render() {
return (
<div>
{this.buildSectionContainer()}
</div>
);
}
The SectionContainer gets the array of objects and generate the document from it according to the type of these sections. The problem is that these sections are not updated when a different doc is selected in the parent component. I see change only when a bigger length array is passed to the child component. Like the firstly selected doc had an array of 2 elements, and then the newly selected doc had 3 elements array of sections and this third section is added to the previously existing 2, but the first 2 sections remained as they were.
And that’s why i though it’s better to unmount the child component and create a new one.
Surely it can happen that i miss something fundamental here again. Maybe related to how react handles lists. I just dont know what.
Edit2:
Ok, figured out that there is a problem with how i use the QuillEditor component. I just dont know what. :) The document updates, only the content of QuillEditors doesnt.
The reason your current solution doesn't actually do anything is because React's state updates are batched, such that, when setState is called a bunch of times in one go, React "combines" the result of all of them. It's not as much of a problem with being "not fast enough" as it is React performing only the work that is necessary.
// this...
this.setState({ message: 'hello', secret: 123 })
this.setState({ message: 'world' })
// ...becomes this
this.setState({ message: 'world', secret: 123 })
This behavior doesn't really have much to do with the problem at hand, though. As long as your UI is a direct translation of state -> view, the UI should simply update in accordance to the state.
class Example extends React.Component {
state = {
documentList: [], // assuming this comes from the server
document: null,
}
// consider making this function accept a document object instead,
// then you could leave out the .find(...) call
handleDocumentSelection = documentName => {
const document = this.state.documentList.find(doc => doc.name === documentName)
this.setState({ document })
}
render() {
const { document } = this.state
return (
<div>
<DocumentList
documents={this.state.documentList}
onDocumentSelection={this.handleDocumentSelection}
/>
{/*
consider having this component accept the entire document
to make it a little cleaner
*/}
{document && <DocumentViewer document={document.content.sections} />}
</div>
)
}
}

React DnD: Avoid using findDOMNode

I don't fully understand it but apparently it isn't recommended to use findDOMNode().
I'm trying to create drag and drop component but I'm not sure how I should access refs from the component variable. This is an example of what I currently have:
const cardTarget = {
hover(props, monitor, component) {
...
// Determine rectangle on screen
const hoverBoundingRect = findDOMNode(component).getBoundingClientRect();
...
}
}
Source
Edit
It might be caused by my component being both the drag and drop source and target as I can get it to work in this example but not this one.
Assuming you're using es6 class syntax and the most recent version of React (15, at time of writing), you can attach a callback ref like Dan did in his example on the link you shared. From the docs:
When the ref attribute is used on an HTML element, the ref callback receives the underlying DOM element as its argument. For example, this code uses the ref callback to store a reference to a DOM node:
<h3
className="widget"
onMouseOver={ this.handleHover.bind( this ) }
ref={node => this.node = node}
>
Then you can access the node just like we used to do with our old friends findDOMNode() or getDOMNode():
handleHover() {
const rect = this.node.getBoundingClientRect(); // Your DOM node
this.setState({ rect });
}
In action:
https://jsfiddle.net/ftub8ro6/
Edit:
Because React DND does a bit of magic behind the scenes, we have to use their API to get at the decorated component. They provide getDecoratedComponentInstance() so you can get at the underlying component. Once you use that, you can get the component.node as expected:
hover(props, monitor, component) {
const dragIndex = monitor.getItem().index;
const hoverIndex = props.index;
const rawComponent = component.getDecoratedComponentInstance();
console.log( rawComponent.node.getBoundingClientRect() );
...
Here it is in action:
https://jsfiddle.net/h4w4btz9/2/
Better Solution
A better solution is to just wrap your draggable component with a div, define a ref on that and pass it to the draggable component, i.e.
<div key={key} ref={node => { this.node = node; }}>
<MyComponent
node={this.node}
/>
</div>
and MyComponent is wrapped in DragSource. Now you can just use
hover(props, monitor, component) {
...
props.node && props.node.getBoundingClientRect();
...
}
(props.node && is just added to avoid to call getBoundingClientRect on an undefined object)
Alternative for findDOMNode
If you don't want to add a wrapping div, you could do the following.
The reply of #imjared and the suggested solution here don't work (at least in react-dnd#2.3.0 and react#15.3.1).
The only working alternative for findDOMNode(component).getBoundingClientRect(); which does not use findDOMNode is:
hover(props, monitor, component) {
...
component.decoratedComponentInstance._reactInternalInstance._renderedComponent._hostNode.getBoundingClientRect();
...
}
which is not very beautiful and dangerous because react could change this internal path in future versions!
Other (weaker) Alternative
Use monitor.getDifferenceFromInitialOffset(); which will not give you precise values, but is perhaps good enough in case you have a small dragSource. Then the returned value is pretty predictable with a small error margin depending on the size of your dragSource.
React-DnD's API is super flexible—we can (ab)use this.
For example, React-DnD lets us determine what connectors are passed to the underlying component. Which means we can wrap them, too. :)
For example, let's override the target connector to store the node on the monitor. We will use a Symbol so we do not leak this little hack to the outside world.
const NODE = Symbol('Node')
function targetCollector(connect, monitor) {
const connectDropTarget = connect.dropTarget()
return {
// Consumer does not have to know what we're doing ;)
connectDropTarget: node => {
monitor[NODE] = node
connectDropTarget(node)
}
}
}
Now in your hover method, you can use
const node = monitor[NODE]
const hoverBoundingRect = node.getBoundingClientRect()
This approach piggybacks on React-DnD's flow and shields the outside world by using a Symbol.
Whether you're using this approach or the class-based this.node = node ref approach, you're relying on the underlying React node. I prefer this one because the consumer does not have to remember to manually use a ref other than the ones already required by React-DnD, and the consumer does not have to be a class component either.

How to allow child component to react to a routing event before the parent component?

I am using react, react-router & redux. The structure of my app is such:
CoreLayout
-> <MaterialToolbar /> (contains back button)
-> {children} (react-router)
When the user presses the back button, which is normally handled by the CoreLayout, I would like the current child component to handle the back button instead of the parent. (In my case, I would like the current view to check if its data has been modified, and pop up an 'Are you sure you wish to cancel?' box before actually going back.) If the child does not wish to handle this, the parent will do it's thing.
Another example would be allowing a childview to set the title in the toolbar.
My reading has told me that accessing a component through a ref and calling a method on it is not the react way -- this is also made a bit more difficult since I am using redux-connect. What is the correct way to implement this behavior?
This is how I would do it, assuming you mean your navigation back button (and not the browser back button):
class CoreLayout extends Component {
handleBack () {
//... use router to go back
}
render () {
return <div>
<MaterialToolbar />
{React.children.map(this.props.children, child => React.cloneElement(child, { onBack: this.handleBack }))}
</div>
}
}
class Child extends Component {
handleBackButtonClick () {
// Here perform the logic to decide what to do
if (dataHasBeenModifiedAndConfirmed) {
// Yes, user wants to go back, call function passed by the parent
this.props.onBack()
} else {
// User didn't confirm, decide what to do
}
}
render () {
return <div onClick={this.handleBackButtonClick.bind(this)}>
Go Back
</div>
}
}
You simply pass a function from the parent to the child via props. Then in the child you can implement the logic to check if you really want to delegate the work to the parent component.
Since you use react-router and your children are passed to your parent component through this.props.children, to pass the onBack function you need to map the children and use React.cloneElement to pass your props (see this answer if you need more details on that: React.cloneElement: pass new children or copy props.children?).
Edit:
Since it seems you want to let the children decide, you can do it this way (using refs):
class CoreLayout extends Component {
constructor () {
super()
this.childRefs = {};
}
handleBack () {
for (let refKey in Object.keys(this.childRefs) {
const refCmp = this.childRefs[refKey];
// You can also pass extra args to refCmp.shouldGoBack if you need to
if (typeof refCmp.shouldGoBack === 'function' && !refCmp.shouldGoBack()) {
return false;
}
}
// No child requested to handle the back button, continue here...
}
render () {
return <div>
<MaterialToolbar />
{React.children.map(this.props.children, (child, n) => React.cloneElement(child, {
ref: cmp => { this.childRefs[n] = cmp; }
}))}
</div>
}
}
class Child extends Component {
shouldGoBack () {
// Return true/false if you do/don't want to actually go back
return true
}
render () {
return <div>
Some content here
</div>
}
}
This is a bit more convoluted as normally with React it's easier/more idiomatic to have a "smart" parent that decides based on the state, but given your specific case (back button in the parent and the logic in the children) and without reimplementing a few other things, I think using refs this way is fine.
Alternatively (with Redux) as the other answer suggested, you would need to set something in the Redux state from the children that you can use in the parent to decide what to do.
Hope it's helpful.
I don't think there is a correct way to solve this problem, but there are many ways. If I understand your problem correctly, most of the time the back button onClick handler will be handled within CoreLayout, but when a particular child is rendered that child will handle the onClick event. This is an interesting problem, because the ability to change the functionality of the back button needs to be globally available, or at very least available in CoreLayout and the particular child component.
I have not used redux, but I have used Fluxible and am familar with the Flux architecture and the pub/sub pattern.
Perhaps you can utilize your redux store to determine the functionality of your back button. And your CoreLayout component would handle rendering the prompt. There is a bug with the following code, but I thought I would not delete my answer for the sake of giving you an idea of what I am talking about and hopefully the following code does that. You would need to think through the logic to get this working correctly, but the idea is there. Use the store to determine what the back button will do.
//Core Layout
componentDidMount() {
store.subscribe(() => {
const state = store.getState();
// backFunction is a string correlating to name of function in Core Layout Component
if(state.backFunction) {
// lets assume backFunction is 'showModal'. Execute this.showModal()
// and let it handle the rest.
this[state.backFunction]();
// set function to false so its not called everytime the store updates.
store.dispatch({ type: 'UPDATE_BACK_FUNCTION', data: false})
}
})
}
showModal() {
// update state, show modal in Core Layout
if(userWantsToGoBack) {
this.onBack();
// update store backFunction to be the default onBack
store.dispatch({ type: 'UPDATE_BACK_FUNCTION', data: 'onBack'})
// if they don't want to go back, hide the modal
} else {
// hide modal
}
}
onBack() {
// handle going back when modal doesn't need to be shown
}
The next step is to update your store when the child component mounts
// Child component
componentDidMount(){
// update backFunction so when back button is clicked the appropriate function will be called from CoreLayout
store.dispatch({ type: 'UPDATE_BACK_FUNCTION', data: 'showModal'});
}
This way you don't need to worry about passing any function to your child component you let the state of the store determine which function CoreLayout will call.

React: Best way to update self, but prevent children from updating?

I'm working on a Drag-and-drop implementation (from scratch, not using a DND library), and wanted to limit the number of unnecessary updates during drag.
Dragging the "clone" (which is usually a copy of the original element, but can be an arbitrary placeholder) is achieved by updating a state on a container component (the "Clonetainer") and using that to apply a transform. However, it makes no sense to update the entire subtree during a move, since the only change is the coords of the container.
Here's my solution to that:
const ClonetainerRenderShield = React.createClass({
shouldComponentUpdate: function (newProps) {
return newProps.shouldUpdate;
},
render: function () {
return this.props.children; // Simple pass-through
}
});
const Clonetainer = React.createClass({
componentWillReceiveProps: function (newProps) {
// OR in any further properties that may indicate a move, versus a child update
this.isMoveEvent = this.props.offset !== newProps.offset;
},
render: function () {
const style = { transform: `translate(${this.props.offset.left}px,${this.props.offset.top}px)` };
return <div className="clonetainer-div" style={style}>
<ClonetainerRenderShield shouldUpdate={ !this.isMoveEvent }>
{ this.props.children }
</ClonetainerRenderShield>
</div>;
}
});
(I won't go into the details on the rest of the DND system, except to say that mouse events from an upstream component feed the offset param to the Clonetainer.)
The solution I came up with for stopping the update involved determining whether the Clonetainer was triggered to update because of a move or some other reason (and setting this.isMoveEvent accordingly), then shimming a component between the Clonetainer and the children consisting of nothing more than a shouldComponentUpdate based upon a passed-in prop (shouldUpdate).
This works. I've tested it in a way that shows that it's updating when it ought to and not updating when it shouldn't, but it feels a bit like overkill to have a separate shim component in there simply to block updates from flowing downhill. Is there a way to indicate that a child component should not be updated from its previous state in render, without requiring the child component to include its own shouldComponentUpdate logic?
You should be able to change componentWillReceiveProps to shouldComponentUpdate in the Clonetainer component and cut out the middle man. shouldComponentUpdate takes two parameters, (object nextProps, object nextState) which you can use to compare agains this.state and this.props. Returning true will cause a re-render.
const Clonetainer = React.createClass({
shouldComponentUpdate: function (nextProps, nextState) {
// OR in any further properties that may indicate a move, versus a child update
this.props.offset !== nextProps.offset;
},
render: function () {
const style = { transform: `translate(${this.props.offset.left}px,${this.props.offset.top}px)` };
return <div className="clonetainer-div" style={style}>
{ this.props.children }
</div>;
}
});

Categories

Resources