React: getBoundingClientRect get position of previous render - javascript

first, code sandbox demonstrating the issue.
we have 2 components - DraggableBox which is a wrapper around react-draggable and SimpleArrow which is a much simplified version of react-xarrows(I'm the author).
we can see a visual bug and that's because both DraggableBox and SimpleArrow update their state based on the same DOM frame(and the same react render phase - useEffect), while SimpleArrow should be updated based on the position of DraggableBox.
we could solve it if we would force 2 renders for each render on 'SimpleArrow', and then on the 2'th render 'SimpleArrow' will read the updated 'DraggableBox' position. we can force 2 renders by writing a custom hook:
export const useMultipleRenders = (renders = 2, effect = useEffect) => {
const [, setRender] = useState({});
const reRender = () => setRender({});
const count = useRef(0);
effect(() => {
if (count.current != renders) {
reRender();
count.current += 1;
} else {
count.current = 0;
}
});
};
now we will consume useMultipleRenders() on SimpleArrow and the visual glitch would be fixed.
here's a code sandbox with the fix. you can see this ugly workaround works.
this actually happens all the time in React when accessing the DOM.
you access the dom during a render using a ref (useRef) value, and during this render, you can only have access to what is currently in the dom, which is the result of the previous render, but you actually need the result of the current render!
for example, in SimpleArrow I'm using getBoundingClientRect on the inner of the svg to determine the svg hight and width:
const SimpleArrow = (props) => {
const svgInnersRef = useRef<SVGGElement>(null);
const {
width: gWidth,
height: gHeight
} = svgInnersRef.current?.getBoundingClientRect() ?? { width: 0, height: 0 };
return (
<svg
width={gWidth}
height={gHeight}
// ...
>
<g ref={svgInnersRef}>
{/* ... */}
</g>
</svg>
);
};
but in order to make sure the height and width are updated I have to double render so I get the right dimensions on the last render.
another thing is that the implementation of useMultipleRenders is not safe as it changing ref value during a render. React core members claims that setting ref value during a render is not safe.
what can I do? what are the alternatives?
TLDR;
can I get the most updated position of a DOM element without manually rerender?
how can I manually re-render without changing a ref value during a render(as it is not safe - on React strict mode it will be called twice and normally only once)?

This is an interesting problem and I am not sure I understand fully, but here's a shot.
You could try adding a drag event listener inside of SimpleArrow.js.
When there is a drag, you would check if it is one of your draggable boxes, and then if it is, you update their positions with a setState, which should trigger a rerender of your svg component and with the new positions of the boxes.

Related

React Material-UI slider value keeps resetting to zero

I am building an interface where you choose a radio and it will reveal different form elements.
When a slider is revealed the value change gets set into an object that is rendered on the page.
In this example I have it so it does update the object but the slider no longer moves even though the value is getting correctly set if you click on the line.
https://codesandbox.io/s/shy-dawn-e5zyu
The object I am setting is declared in the parent :
const defaultActivity = {
group: "",
a: 0,
b: 0
};
const [activity, setActivity] = useState(defaultActivity);
Then in the child component that contains the slider the handleChange function is use within that component and I also use a parent function to access setActivity.
const TestSlider = (props) => {
const [value, setValue] = React.useState(0);
const handleChange = (event, newValue) => {
setValue(newValue);
// This sets the slider value to the activity object
// if you comment this line out then the sliders slide correctly
props.onDataChanged(props.name.toLowerCase(), newValue);
};
return (
<React.Fragment>
<Typography id="discrete-slider" gutterBottom>
{props.name}
</Typography>
<Slider
value={value}
aria-labelledby="discrete-slider"
valueLabelDisplay="auto"
step={props.step}
marks
min={0}
max={props.max}
name={props.name}
onChange={handleChange}
/>
</React.Fragment>
);
};
And that onDataChanged method in the parent
const onDataChanged = (name, value) => {
setActivity({
...activity,
[name]: value
});
};
As I noted in the comment there, if you remove the onDataChanged method then the sliders work again. I have tried moving handleChange to the parent as well but that did not help.
After creating this Sandbox I seem to have a new problem that is sporadic and unclear to me steps to replicate where I am getting the error :
Cannot read property 'getBoundingClientRect' of null
Maybe related to broken slider issue or something else I screwed up. Thanks in advance for any help with this, I am trying hard to learn my way around React but this problem has me really stumped.
============= UPDATE: Closer to expected and a new problem =============
Still playing with this and decided to try and use onChangeCommitted on the slider to try and disconnect the value getting set in the slider with it getting set to the main object. Now it slides and updates the value which sticks in the object but gets reset to 0 for the slider UI. Hard to explain so maybe easier if you go play with it and see what I mean. I think the way I am setting the default state is probably the issue but not sure how to refactor this :
https://codesandbox.io/s/sleepy-euler-0dv69?file=/src/TestSlider.js:116-163
The root of the problem is that the component RenderSwitch is created (declared) inside the AddActivitySelect (SelectForm) component. Moving RenderSwitch outside and passing all necessary props fixes the problem. Here is a working codesandbox.
What happened:
When you call props.setActivity in TestSlider, the activity state updates in App
App re-renders which causes SelectForm to re-render also.
Because a completely new RenderSwitch component is created inside SelectForm, the previous one in unmounted (destroyed).
The new RenderSwitch is rendered, calls React.useState(0), and that's why the slider value is 0

Call function only after multiple states have completed updating

Logic:
I have a dialog for converting units. It has two stages of choice for the user: units to convert from and units to convert to. I keep this stage as a state, dialogStage, for maintainability as I'm likely going to need to reference what stage the dialog is in for more features in the future. Right now it's being used to determine what action to take based on what unit is clicked.
I also have a state, dialogUnits, that causes the component to rerender when it's updated. It's an array of JSX elements and it's updated via either foundUnitsArray or convertToUnitsArray, depending on what stage the dialog is at. Currently both states, dialogStage and dialogUnits, are updated at the same moment the problem occurs.
Problem:
When choosing the convertTo units, displayConversionTo() was still being called, as though dialogStage was still set to 'initial' rather than 'concertTo'. Some debugging led to confusion as to why the if (dialogStage == 'initial') was true when I'd set the state to 'convertTo'.
I believe that my problem was that the dialogStage state wasn't updated in time when handleUnitClick() was called as it's asynchronous. So I set up a new useEffect that's only called when dialogStage is updated.
The problem now is that the dialog shows no 'convertTo' units after the initial selection. I believe it's now because dialogUnits hasn't updated in time? I've swapped my original problem from one state not being ready to another state not being ready.
Question
How do I wait until both states are updated before continuing to call a function here (e.g. handleUnitClick()?).
Or have I mistaken what the problem is?
I'm new to react and, so far, I'm only familiar with the practice of state updates automatically rerendering a component when ready, unless overridden. Updating dialogUnits was displaying new units in the dialog until I tried to update it only when dialogStage was ready. It feels like an either/or situation right now (in terms of waiting for states to be updated) and it's quite possible I've overlooked something more obvious, as it doesn't seem to fit to be listening for state updates when so much of ReactJs is built around that already being catered for with rerenders, etc.
Component code:
function DialogConvert(props) {
const units = props.pageUnits;
const [dialogUnits, setDialogUnits] = useState([]);
const [dialogStage, setDialogStage] = useState('initial');
let foundUnitsArray = [];
let convertToUnitsArray = [];
units.unitsFound.forEach(element => {
foundUnitsArray.push(<DialogGroupChoice homogName={element} pcbOnClick={handleUnitClick} />);
});
useEffect(() => {
setDialogUnits(foundUnitsArray);
}, []);
useEffect(() => {
if (dialogStage == "convertTo") {
setDialogUnits(convertToUnitsArray);
}
}, [dialogStage]);
function handleClickClose(event) {
setDialogStage('initial');
props.callbackFunction("none");
}
function handleUnitClick(homogName) {
if (dialogStage == "initial") {
// getConversionChoices is an external function that returns an array. This returns fine and as expected
const choices = getConversionChoices(homogName);
displayConversionTo(choices);
} else if (dialogStage == "convertTo") {
// Can't get this far
// Will call a function not displayed here once it works
}
}
function displayConversionTo(choices) {
let canConvertTo = choices[0]["canconvertto"];
if (canConvertTo.length > 0) {
canConvertTo.forEach(element => {
convertToUnitsArray.push(<DialogGroupChoice homogName={element} pcbOnClick={handleUnitClick} />);
});
setDialogStage('convertTo');
}
}
return (
<React.Fragment>
<div className="dialog dialog__convertunits" style={divStyle}>
<h2 className="dialogheader">Convert Which unit?</h2>
<div className='js-dialogspace-convertunits'>
<ul className="list list__convertunits">
{dialogUnits}
</ul>
</div>
<button className='button button__under js-close-dialog' onClick={handleClickClose}>Close</button>
</div>
</React.Fragment>
)
}
So, there are some issues with your implementations:
Using non-state variables to update the state in your useEffect:
Explanation:
In displayConversionTo when you run the loop to push elements in convertToUnitsArray, and then set the state dialogStage to convertTo, you should be facing the issue that the updated values are not being rendered, as the change in state triggers a re-render and the convertToUnitsArray is reset to an empty array because of the line:
let convertToUnitsArray = [];
thus when your useEffect runs that is supposed to update the
dialogUnits to convertToUnitsArray, it should actually set the dialogueUnits to an empty array, thus in any case the updated units should not be visible on click of the initial units list.
useEffect(() => {
if (dialogStage == "convertTo") {
// as your convertToUnitsArray is an empty array
// your dialogue units should be set to an empty array.
setDialogUnits(convertToUnitsArray)
}
}, [dalogStage]);
You are trying to store an array of react components in the state which is not advisable:
http://web.archive.org/web/20150419023006/http://facebook.github.io/react/docs/interactivity-and-dynamic-uis.html#what-components-should-have-state
Also, refer https://stackoverflow.com/a/53976730/10844020
Solution: What you can do is try to save your data in a state, and then render the components using that state,
I have created a code sandbox example how this should look for your application.
I have also made some changes for this example to work correctly.
In your code , since you are passing units as props from parent, can you also pass the foundUnitsArray calculated from parent itself.
setDialogUnits(props.foundUnitsArray);
and remove the below operation,
units.unitsFound.forEach(element => {
foundUnitsArray.push(<DialogGroupChoice homogName={element} pcbOnClick={handleUnitClick} />);
});

MobX observable boolean does not re-render component, but observable number does

I'm getting started with a new create-react-app application using TypeScript, hooks, and mobx-react-lite. Despite having used MobX extensively in a React Native app in the past, I've run into an issue that doesn't make any sense to me.
I have a store with two observables: one number and one boolean. There is an initialize() method that runs some library code, and in the success callback, it sets the number and the boolean to different values (see Line A and Line B below).
The issue: my component ONLY re-renders itself when Line A is present. In that case, after the initialization is complete, the 'ready' text appears, and the button appears. If I delete Line B, the 'ready' text still appears. But if I delete Line A (and keep Line B), the button never renders. I've checked things over a hundred times, everything is imported correctly, I have decorator support turned on. I can't imagine why observing a number can trigger a re-render but observing a boolean cannot. I'm afraid I'm missing something horribly obvious here. Any ideas?
The relevant, simplified code is as follows:
// store/app.store.ts
export class AppStore {
#observable ready = false
#observable x = 5
initialize() {
// Takes a callback
ThirdPartyService.init(() => {
this.ready = true
this.x = 10
})
}
}
// context/stores.ts
const appStore = new AppStore()
const storesContext = React.createContext({
appStore
})
export const useStores = () => React.useContext(storesContext)
// App.tsx
const App = observer(() => {
const { appStore } = useStores()
useEffect(() => {
appStore.initialize()
}, [appStore])
return (
<div>
{ appStore.x === 10 && 'ready' } // <-- Line A
{ appStore.ready && <button>Go</button> } // <-- Line B
</div>
)
}
EDIT: A bit more information. I've added some logging statements to just before the return statement for the App component. I also refactored the button conditional to a const. This may provide more insight:
const button = appStore.ready ? <button>Go</button> : null
console.log('render', appStore.ready)
console.log('button', button)
return (
<div className="App">
<header className="App-header">{button}</header>
</div>
)
When appStore.ready is updated, the component does re-render, but the DOM isn't updated. The console shows 'render' true and shows a representation of the button, as it should, but inspecting the document itself shows no button there. Somehow, though, changing the condition from appStore.ready to appStore.x === 10 does update the DOM.
Turns out I didn't quite give complete information in my question. While I was creating a minimal reproduction, I decided to try dropping the top-level <React.StrictMode> component from index.tsx. Suddenly, everything worked. As it happens, mobx-react-lite#1.5.2, the most up-to-date stable release at the time of my project's creation, does not play nice with Strict Mode. Until it's added to a stable release, the two options are:
Remove strict mode from the React component tree
Use mobx-react-lite#next

React DND - get coordinates of dragged element as the mouse moves

I have an image that when I drag I want to implement a rotation too. The solution I had in mind was to use React DnD to get the xy coordinates of the dragged image position and calculate the difference between the original image location. The value from this difference would form the basis to make the rotation.
I've looked at examples on the ReactDnD library and see that the DragSource Specification can access the Monitor variable. This monitor variable has access to methods like getInitialClientOffset(). When I implement a console.log() of this value it shows me the last last coordinate set when I release the mouse.
Using this library, is there an easy way to get the current position of the dragged element as I move the mouse?
import React from 'react'
import { DragSource } from 'react-dnd'
// Drag sources and drop targets only interact
// if they have the same string type.
// You want to keep types in a separate file with
// the rest of your app's constants.
const Types = {
CARD: 'card',
}
/**
* Specifies the drag source contract.
* Only `beginDrag` function is required.
*/
const cardSource = {
beginDrag(props,monitor,component) {
// Return the data describing the dragged item
const clientOffset = monitor.getSourceClientOffset();
const item = { id: props.id }
console.log(clientOffset);
return item
},
isDragging(props, monitor){
console.log(monitor.getClientOffset())
},
endDrag(props, monitor, component) {
if (!monitor.didDrop()) {
return
}
// When dropped on a compatible target, do something
const item = monitor.getItem()
const dropResult = monitor.getDropResult()
console.log(item,dropResult)
//CardActions.moveCardToList(item.id, dropResult.listId)
},
}
/**
* Specifies which props to inject into your component.
*/
function collect(connect, monitor) {
return {
// Call this function inside render()
// to let React DnD handle the drag events:
connectDragSource: connect.dragSource(),
// You can ask the monitor about the current drag state:
isDragging: monitor.isDragging(),
}
}
function Card(props) {
// Your component receives its own props as usual
const { id } = props
// These two props are injected by React DnD,
// as defined by your `collect` function above:
const { isDragging, connectDragSource } = props
return connectDragSource(
<div>
I am a draggable card number {id}
{isDragging && ' (and I am being dragged now)'}
</div>,
)
}
// Export the wrapped version
export default DragSource(Types.CARD, cardSource, collect)(Card)
From what I remember, custom drag layer had serious performance issues. It's better to directly subscribe to the underlying API (the official documentation is severely lacking, but you can get this information from reading drag layer source):
const dragDropManager = useDragDropManager();
const monitor = dragDropManager.getMonitor();
React.useEffect(() => monitor.subscribeToOffsetChange(() => {
const offset = monitor.getClientOffset();
// do stuff like setState, though consider directly updating style through refs for performance
}), [monitor]);
You can also use some flag based on drag state to only subscribe when needed (simple isDragging && monitor.subscribe... plus dependencies array does the trick).
I'm posting this way after the fact, but in case anyone is looking for this answer, the react-dnd way of doing this is by using what they call a Drag Layer - it gives you a way to use a custom component to be displayed when dragging.
They have a full example here:
https://codesandbox.io/s/github/react-dnd/react-dnd/tree/gh-pages/examples_hooks_js/02-drag-around/custom-drag-layer?from-embed=&file=/src/CustomDragLayer.jsx
Also in the docs you want to look at useDragLayer and DragLayerMonitor
The useDrop hook has a hover(item, monitor) method which does what you are looking for. It triggers repeatedly while you are hovering over the drop target.
You can use
ReactDOM.findDOMNode(component).getBoundingClientRect();
Ref: https://reactjs.org/docs/react-dom.html#finddomnode
Or just make a ref to your component instance and get getBoundingClientRect() on the instance within a onmousemove event.
As far as I understand all you need offset of currently dragging element, via HTML5Backend you can't get, but if you use MouseBackend you can easily get the coordinates
https://github.com/zyzo/react-dnd-mouse-backend
see example of with preview

Render Child Component after Parent Component componentDidMount finishes

I have a third party library Im trying to use. It has a particular prop that allows you to pass in a string it uses to get a DOM element and return the bottom value.
<Sticky bottomBoundary="#some-id">
<MyChildComponentMightBeANavigationOrAnything />
</Sticky>
The component takes the id and determines the bottom value so it knows when to release itself from the sticky status. This id is basically another element in the DOM. So when that elements bottom value reaches the top of the view port the sticky component is allowed to move up as the user scrolls. The problem Im having is I need to add an offset. The Sticky component allows you to pass in a number value instead.
<Sticky bottomBoundary={1200}>
<MyChildComponentMightBeANavigationOrAnything />
</Sticky>
I need to add an offset of whatever the sticky elements height is. So lets say that "#some-id" element was 1200px and the height of the sticky element was 50, I need to to be able to get the "#some-id" and subtract 50 before passing the value into bottomBoundary={}. My calculated value would be bottomBoundary={1150}.
I tried the following. I created a component that wraps Sticky as follows:
export class WrapperSticky extends React.Component {
constructor(props) {
super(props);
this.boundary = null;
}
componentDidMount() {
const el = document.querySelector(this.props.bottomBoundary);
const rect: any = el.getBoundingClientRect();
this.boundary = rect.bottom - 50;
console.log(this.boundary);
}
render() {
return (
<Sticky innerZ={2000} bottomBoundary={this.boundary}>{this.props.children}</Sticky>
);
}
}
And I added the markup as follows:
<WrapperSticky bottomBoundary="#hero" offset={true}>
<MyChildComponentMightBeANavigationOrAnything />
</WrapperSticky >
Inside the WrapperSticky I attempted to do the calculations in the componentDidMount method and pass the results into the Sticky component. The obvious problem is the Sticky component tries to find the value before the wrapper component has completed the calculations.
Is there a way to do this elegantly. I am very new to react so any articles or documentation to learn from would be a plus.
Thanks.
You need to use the component state for this. And after calculation finished - update the state, so component re-renders with calculated values.
this.state.boundary vs this.boundary
Putting boundary value into component's state will help you by re-rendering on any of its change (i.e. setState call).
While plain class fields should be used only if a value should not affect render result.
Here is the code:
class WrapperSticky extends Component {
state = {
boundary: undefined,
}
componentDidMount() {
const el = document.querySelector(this.props.bottomBoundary)
const rect = el.getBoundingClientRect()
const boundary = rect.bottom - 50
this.setState({ boundary })
}
render() {
const { boundary } = this.state
return boundary === undefined
? null // or placeholder
: (
<Sticky innerZ={2000} bottomBoundary={boundary}>
{this.props.children}
</Sticky>
)
}
}
You could have a boolean flag in the state of WrapperSticky for whether you've done the calculation yet. It's initially false, and render returns <></>. In componentDidMount, after you do the calculation, you set the flag to true, which triggers a re-render that renders the child for real. This should at least work (I've done something similar here using the subProps state field), though there might be a better way using some of the more advanced lifecycle hooks.

Categories