Get total width of JSX Element - javascript

How could I get the width of a JSX.Element? If I was in vanilla, I would do something like
const button = document.createElement('button');
document.body.appendChild(button)
window.getComputedStyle(button).width
Now I need to do the same, but it seems ref is null and I'm not sure even how to temporarily append to the DOM just to see what its width would be.
const button: JSX.Element = <CustomButton/>;
/// ....

Couple of things to check and consider.
where is the ref created and does it get forwarded correctly to a
valid dom element (make sure that CustomButton uses forwardRef).
you don't need to append anything in react to look at the width. all you need is ref.current.clientWidth or ref.current.getBoundingClientRect(), but ref.current has to exist in the first place :-)
if you need access to the ref.current element when your component first mounts (and not in a onClick or some other callback - then this does not apply) you'll have to use useLayoutEffect as the javascript runs before the dom is rendered so there is technically no to measure yet.
See this example:
ParentComponent.tsx
import {useState, useLayoutEffect} from 'react';
const ParentComponent = () => {
const [width, setWidth] = useState<number | null>(null);
useLayoutEffect(() => {
if(ref?.current && !width) {
const { clientWidth } = ref.current;
setWidth(clientWidth);
}
}, [ref?.current]);
console.log('width', width);
// `width` will be null at first render,
// then when CustomButton renders and <button> is created the ref will be
// updated, triggering your layout side effect that saves the
// clientWidth to the state. State change will trigger a rerender of
// ParentComponent and your console.log will finally print the width (whose
// value is stored in the state).
return <CustomButton ref={ref}/>;
};
CustomButton.tsx
import {forwardRef} from 'react';
const CustomButton = forwarRef((props, ref) => {
return (
<>
// some other stuff
<button ref={ref}/>>Click</button>
</>
);
};

The canonical way is to use a ref, and then observe it within an effect, which is called after the DOM gets rendered:
const ref = useRef();
useEffect(() => {
console.log(window.getComputedStyle(ref.current).width)
}, []);
return <button ref={ref}/>;

Related

call a function after rendering ends react

I have a function which gets all the inputs rendered (querySelector) in order to execute other stuff and also is called by the "onChange" method of a component I have in order to detect if other inputs were added.
The main problem here is that when useEffect() is called, for some reason not all the inputs are detected, for example if in the DOM there are 3 inputs it only detects 2 (the last input added is not being detected as you can see in the example).
what I think is happening is that the querySelector function ends before the other input is rendered so thats why only the last one added is not being detected. I tried adding a setTimeout() function but that is not a good practice...
Any recommendations?
const onChangeQuery = (e) => {
//some other code here...
selectFirstOpt();
};
useEffect(() => {
selectFirstOpt();
},[])
const selectFirstOpt = () => {
let selects = document.querySelectorAll(".rule--field > select");
if(!selects.length) return;
selects.forEach((select) => {
let firstOpt = select.options[0];
if(firstOpt.value === "") firstOpt.innerText = "select an option";
})
}
I think you could use useRef with useEffect. Also using useRef you can work on the element instead of using selectors to change element values.
import React, { useRef, useEffect } from 'react';
...
/* In your react hook function*/
const first = useRef();
const second = useRef();
...
useEffect(() => {
first.current.value = "value";
second.current.value = "changed";
}, [first, second])
...
/* in your return function */
<input type="text" ref={first} />
<input type="text" ref={second} />
This will change the value of the inputs as soon as they are loaded correctly, it is important to add useRef variables or constants into useEffect square brackets.

using useRef or createRef

I want to set the const [solution, setSolution] = useState(0); with the value of a input element by pressing a button
I get the same result using createRef or using the useRef hook
reading What's the difference between useRef and createRef?
gives quit different answers what exactly thees to do, is there a clear inside about thees to methods ?
function Interface() {
const [solution, setSolution] = useState(0);
const solutionInp = useRef();
// --createRef();
const onButtonClick = () => {
// `current` points to the mounted text input element
setSolution( solutionInp.current.value)
};
return (
<input
type="text"
// value={solution}
ref={solutionInp}
// onInput={(e) => setSolution(e.target.value)}
/>
</section>
<button type="button" onClick={onButtonClick}>do</button>
)}
createRef is for class components, calling it in a context of function component will be treated as a normal function, hence will RE-INITIAL your reference on every render.
useRef is for function components, you lose your ref on unmount lifecycle.

Unexpected behavior with useState hook React

I have some experience with class components in React, but am trying to learn hooks and functional components better.
I have the following code:
import React, { useState } from "react";
import { Button } from "reactstrap";
import StyleRow from "./StyleRow";
export default function Controls(props) {
const [styles, setStyles] = useState([]);
function removeStyle(index) {
let newStyles = styles;
newStyles.splice(index, 1);
setStyles(newStyles);
}
return (
<div>
{styles}
<Button
color="primary"
onClick={() => {
setStyles(styles.concat(
<div>
<StyleRow />
<Button onClick={() => removeStyle(styles.length)}>x</Button>
</div>
));
}}
>
+
</Button>
</div>
);
}
The goal of this code is to have an array of components that have an "x" button next to each one that removes that specific component, as well as a "+" button at the bottom that adds a new one. The StyleRow component just returns a paragraph JSX element right now.
The unusual behavior is that when I click the "x" button by a row, it removes the element and all elements following it. It seems that each new StyleRow component that is added takes the state of styles at the moment of its creation and modifies that instead of always modifying the current styles state. This is different behavior than I would expect from a class component.
The freezing of state leads me to believe this has something to do with closures, which I don't fully understand, and I am curious to know what here triggered them. If anyone knows how to solve this problem and always modify the same state, I would greatly appreciate it.
Finally, I think this post on SO is similar, but I believe it addresses a slightly different question. If someone can explain how that answer solves this problem, of course feel free to close this question. Thank you in advance!
You are modifying the existing state of styles, so you will need to create a deep copy of the array first.
You can either write your own clone function, or you can import the Lodash cloneDeep function.
Add the following dependency to your package.json using:
npm install lodash
Also, you are passing the length of the array to the removeStyle function. You should be passing the last index which is length - 1.
// ...
import { cloneDeep } from 'lodash';
// ...
function removeStyle(index) {
let newStyles = cloneDeep(styles); // Copy styles
newStyles.splice(index, 1); // Splice from copy
setStyles(newStyles); // Assign copy to styles
}
// ...
<Button onClick={() => removeStyle(styles.length - 1)}>x</Button>
// ...
If you want to use a different clone function or write your own, there is a performance benchmark here:
"What is the most efficient way to deep clone an object in JavaScript?"
I would also move the function assigned to the onClick event handler in the button outside of the render function. It looks like you are calling setStyles which adds a button with a removeStyle event which itself calls setStyles. Once you move it out, you may be able to better diagnose your issue.
Update
I rewrote your component below. Try to render elements using the map method.
import React, { useState } from "react";
import { Button } from "reactstrap";
const Controls = (props) => {
const [styles, setStyles] = useState([]);
const removeStyle = (index) => {
const newStyles = [...styles];
newStyles.splice(index, 1);
setStyles(newStyles);
};
const getChildNodeIndex = (elem) => {
let position = 0;
let curr = elem.previousSibling;
while (curr != null) {
if (curr.nodeType !== Node.TEXT_NODE) {
position++;
}
curr = curr.previousSibling;
}
return position;
};
const handleRemove = (e) => {
//removeStyle(parseInt(e.target.dataset.index, 10));
removeStyle(getChildNodeIndex(e.target.closest("div")));
};
const handleAdd = (e) => setStyles([...styles, styles.length]);
return (
<div>
{styles.map((style, index) => (
<div key={index}>
{style}
<Button data-index={index} onClick={handleRemove}>
×
</Button>
</div>
))}
<Button color="primary" onClick={handleAdd}>
+
</Button>
</div>
);
};
export default Controls;
I've added the most preferred way in a new answer as the previous one was becoming too long.
The explanation lies in my previous answer.
import React, { useState } from "react";
import { Button } from "reactstrap";
export default function Controls(props) {
const [styles, setStyles] = useState([]);
function removeStyle(index) {
let newStyles = [...styles]
newStyles.splice(index, 1);
setStyles(newStyles);
}
const addStyle = () => {
const newStyles = [...styles];
newStyles.push({content: 'ABC'});
setStyles(newStyles);
};
// we are mapping through styles and adding removeStyle newly and rerendering all the divs again every time the state updates with new styles.
// this always ensures that the removeStyle callback has reference to the latest state at all times.
return (
<div>
{styles.map((style, index) => {
return (
<div>
<p>{style.content} - Index: {index}</p>
<Button onClick={() => removeStyle(index)}>x</Button>
</div>
);
})}
<Button color="primary" onClick={addStyle}>+</Button>
</div>
);
}
Here is a CodeSandbox for you to play around.
Let's try to understand what's going on here.
<Button
color="primary"
onClick={() => {
setStyles(styles.concat(
<div>
<StyleRow />
<Button onClick={() => removeStyle(styles.length)}>x</Button>
</div>
));
}}
>
+
</Button>
First render:
// styles = []
You add a new style.
// styles = [<div1>]
The remove callback from the div is holding the reference to styles, whose length is now 0
You add one more style. // styles = [<div1>, <div2>]
Since div1 was created previously and didn't get created now, its still holding a reference to styles whose length is still 0.
div2 is now holding a reference to styles whose length is 1.
Now the same goes for the removeStyle callback that you have. Its a closure, which means it's holding a reference to a value of its outer function, even after the outer function has done executing. So when removeStyles is called for the first div1 the following lines will execute:
let newStyles = styles; // newStyles []
newStyles.splice(index, 1); // index(length) = 0;
// newStyles remain unchanged
setStyles(newStyles); // styles = [] (new state)
Now consider you have added 5 styles. So this is how the references will be held by each div
div1 // styles = [];
div2 // styles = [div1];
div3 // styles = [div1, div2];
div4 // styles = [div1, div2, div3];
div5 // styles = [div1, div2, div3, div4];
So what happens if you try to remove div3, the following removeStyly will execute:
let newStyles = styles; // newStyles = [div1, div2]
newStyles.splice(index, 1); // index(length) = 2;
// newStyles remain unchanged; newStyles = [div1, div2]
setStyles(newStyles); // styles = [div2, div2] (new state)
Hope that helps and addresses your concern. Feel free to drop any questions in the comments.
Here is a CodeSandbox for you to play around with and understand the issue properly.

React Native - need variable to update immediately, but useState is async?

Ok, I've looked extensively at useState set method not reflecting change immediately and I understand that React.useState variables are async.
This is a problem given that I'm working with a component structured React Native app, and need to set and use a variable immediately upon releasing drag of a component. I am doing this within a callback function of a Draggable from https://github.com/tongyy/react-native-draggable#readme , so the callback hook solution from useState set method not reflecting change immediately won't work.
Here's what I mean. I start with these React.useState
export default function App() {
const [firstColor, setFirstColor] = React.useState('');
const [secondColor, setSecondColor] = React.useState('');
Then I update them based on the position of the Draggable on the OnDragRelease callback function:
<Draggable onDragRelease={(event, gestureState) => { ToggleComboColor1(event, gestureState); ToggleComboPage(); } } onPressIn={() => {setDidDrag1(true);}} />
Here's where there is trouble. I have to set the useState variable based on gestureState, then if both Draggables have been dragged, toggle a result with ToggleComboPage() :
const ToggleComboColor1 = (event, gestureState) => {
setFirstColor(determineDraggableColor(gestureState.moveX, gestureState.moveY));
console.log(firstColor); //This does NOT print an updated value
}
const ToggleComboPage = () =>
{
if(didDrag1 && didDrag2)
{
console.log(firstColor + " | "+secondColor); //This, consequently, does NOT print an updated value for the most recently dragged
// setCurrentColorCombo(
// getColorComboItemArray(firstColor, secondColor)[0]
// );
// setCurrentKey("Combo");
// setDidDrag1(false);
// setDidDrag2(false);
}
}
I tried using a plain var to circumvent this, but those are re rendered / re created every time there's a re render.
How can I update and use these variables here?

How does React.useState triggers re-render?

import { useState } from 'react';
function Example() {
const [count, setCount] = useState(0);
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
);
}
In the above example whenever setCount(count + 1) is invoked a re-render happens. I am curious to learn the flow.
I tried looking into the source code. I could not find any reference of useState or other hooks at github.com/facebook/react.
I installed react#next via npm i react#next and found the following at node_modules/react/cjs/react.development.js
function useState(initialState) {
var dispatcher = resolveDispatcher();
return dispatcher.useState(initialState);
}
On tracing back for dispatcher.useState(), I could only find the following ...
function resolveDispatcher() {
var dispatcher = ReactCurrentOwner.currentDispatcher;
!(dispatcher !== null) ? invariant(false, 'Hooks can only be called inside the body of a function component.') : void 0;
return dispatcher;
}
var ReactCurrentOwner = {
/**
* #internal
* #type {ReactComponent}
*/
current: null,
currentDispatcher: null
};
I wonder where can I find dispatcher.useState() implementation and learn how it triggers re-render when setState setCount is invoked.
Any pointer would be helpful.
Thanks!
The key in understanding this is the following paragraph from the Hooks FAQ
How does React associate Hook calls with components?
React keeps track of the currently rendering component. Thanks to the Rules of Hooks, we know that Hooks are only called from React components (or custom Hooks — which are also only called from React components).
There is an internal list of “memory cells” associated with each component. They’re just JavaScript objects where we can put some data. When you call a Hook like useState(), it reads the current cell (or initializes it during the first render), and then moves the pointer to the next one. This is how multiple useState() calls each get independent local state.
(This also explains the Rules of Hooks. Hooks need to be called unconditionally in the same order, otherwise the association of memory cell and hook is messed up.)
Let's walk through your counter example, and see what happens. For simplicity I will refer to the compiled development React source code and React DOM source code, both version 16.13.1.
The example starts when the component mounts and useState() (defined on line 1581) is called for the first time.
function useState(initialState) {
var dispatcher = resolveDispatcher();
return dispatcher.useState(initialState);
}
As you have noticed, this calls resolveDispatcher() (defined on line 1546). The dispatcher refers internally to the component that's currently being rendered. Within a component you can (if you dare to get fired), have a look at the dispatcher, e.g. via
console.log(React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.ReactCurrentDispatcher.current)
If you apply this in case of the counter example, you will notice that the dispatcher.useState() refers to the react-dom code. When the component is first mounted, useState refers to the one defined on line 15986 which calls mountState(). Upon re-rendering, the dispatcher has changed and the function useState() on line 16077 is triggered, which calls updateState(). Both methods, mountState() on line 15352 and updateState() on line 15371, return the count, setCount pair.
Tracing ReactCurrentDispatcher gets quite messy. However, the fact of its existence is already enough to understand how the re-rendering happens. The magic happens behind the scene. As the FAQ states, React keeps track of the currently rendered component. This means, useState() knows which component it is attached to, how to find the state information and how to trigger the re-rendering.
setState is a method on the Component/PureComponent class, so it will do whatever is implemented in the Component class (including calling the render method).
setState offloads the state update to enqueueSetState so the fact that it's bound to this is really only a consequence of using classes and extending from Component. Once, you realize that the state update isn't actually being handled by the component itself and the this is just a convenient way to access the state update functionality, then useState not being explicitly bound to your component makes much more sense.
I also tried to understand the logic behind useState in a very simplified and basic manner, if we just look into its basic functionalities, excluding optimizations and async behavior, then we found that it is basically doing 4 things in common,
maintaining of State, primary work to do
re-rendering of the component through which it get called so that caller component can get the latest value for state
as it caused the re-rendering of the caller component it means it must maintain the instance or context of that component too, which also allows us to use useState for multiple component at once.
as we are free to use as many useState as we want inside our component that means it must maintain some identity for each useState inside the same component.
keeping these things in mind I come up with the below snippet
const Demo = (function React() {
let workInProgress = false;
let context = null;
const internalRendering = (callingContext) => {
context = callingContext;
context();
};
const intialRender = (component) => {
context = component;
workInProgress = true;
context.state = [];
context.TotalcallerId = -1; // to store the count of total number of useState within a component
context.count = -1; // counter to keep track of useStates within component
internalRendering(context);
workInProgress = false;
context.TotalcallerId = context.count;
context = null;
};
const useState = (initState) => {
if (!context) throw new Error("Can only be called inside function");
// resetting the count so that it can maintain the order of useState being called
context.count =
context.count === context.TotalcallerId ? -1 : context.count;
let callId = ++context.count;
// will only initialize the value of setState on initial render
const setState =
!workInProgress ||
(() => {
const instanceCallerId = callId;
const memoizedContext = context;
return (updatedState) => {
memoizedContext.state[instanceCallerId].value = updatedState;
internalRendering(memoizedContext);
};
})();
context.state[callId] = context.state[callId] || {
value: initState,
setValue: setState,
};
return [context.state[callId].value, context.state[callId].setValue];
};
return { useState, intialRender };
})();
const { useState, intialRender } = Demo;
const Component = () => {
const [count, setCount] = useState(1);
const [greeting, setGreeting] = useState("hello");
const changeCount = () => setCount(100);
const changeGreeting = () => setGreeting("hi");
setTimeout(() => {
changeCount();
changeGreeting();
}, 5000);
return console.log(`count ${count} name ${greeting}`);
};
const anotherComponent = () => {
const [count, setCount] = useState(50);
const [value, setValue] = useState("World");
const changeCount = () => setCount(500);
const changeValue = () => setValue("React");
setTimeout(() => {
changeCount();
changeValue();
}, 10000);
return console.log(`count ${count} name ${value}`);
};
intialRender(Component);
intialRender(anotherComponent);
here useState and initialRender are taken from Demo. intialRender is use to call the components initially, it will initialize the context first and then on that context set the state as an empty array (there are multiple useState on each component so we need array to maintain it) and also we need counter to make count for each useState, and TotalCounter to store total number of useState being called for each component.
FunctionComponent is different. In the past, they are pure, simple. But now they have their own state.
It's easy to forget that react use createElement wrap all the JSX node, also includes FunctionComponent.
function FunctionComponent(){
return <div>123</div>;
}
const a=<FunctionComponent/>
//after babel transform
function FunctionComponent() {
return React.createElement("div", null, "123");
}
var a = React.createElement(FunctionComponent, null);
The FunctionComponent was passed to react. When setState is called, it's easy to re-render;

Categories

Resources