How to add "refs" dynamically with react hooks? - javascript

So I have an array of data in and I am generating a list of components with that data. I'd like to have a ref on each generated element to calculate the height.
I know how to do it with a Class component, but I would like to do it with React Hooks.
Here is an example explaining what I want to do:
import React, {useState, useCallback} from 'react'
const data = [
{
text: 'test1'
},
{
text: 'test2'
}
]
const Component = () => {
const [height, setHeight] = useState(0);
const measuredRef = useCallback(node => {
if (node !== null) {
setHeight(node.getBoundingClientRect().height);
}
}, []);
return (
<div>
{
data.map((item, index) =>
<div ref={measuredRef} key={index}>
{item.text}
</div>
)
}
</div>
)
}

Not sure i fully understand your intent, but i think you want something like this:
const {
useState,
useRef,
createRef,
useEffect
} = React;
const data = [
{
text: "test1"
},
{
text: "test2"
}
];
const Component = () => {
const [heights, setHeights] = useState([]);
const elementsRef = useRef(data.map(() => createRef()));
useEffect(() => {
const nextHeights = elementsRef.current.map(
ref => ref.current.getBoundingClientRect().height
);
setHeights(nextHeights);
}, []);
return (
<div>
{data.map((item, index) => (
<div ref={elementsRef.current[index]} key={index} className={`item item-${index}`}>
{`${item.text} - height(${heights[index]})`}
</div>
))}
</div>
);
};
const rootElement = document.getElementById("root");
ReactDOM.render(<Component />, rootElement);
.item {
box-sizing: border-box;
display: flex;
align-items: center;
justify-content: center;
border: 1px solid #ccc;
}
.item-0 {
height: 25px;
}
.item-1 {
height: 50px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.6/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.6/umd/react-dom.production.min.js"></script>
<div id="root"/>

I created a tiny npm package that exposes a React Hook to handle setting and getting refs dynamically as I often run into the same problem.
npm i use-dynamic-refs
Here's a simple example.
import React, { useEffect } from 'react';
import useDynamicRefs from 'use-dynamic-refs';
const Example = () => {
const foo = ['random_id_1', 'random_id_2'];
const [getRef, setRef] = useDynamicRefs();
useEffect(() => {
// Get ref for specific ID
const id = getRef('random_id_1');
console.log(id)
}, [])
return (
<>
{/* Simple set ref. */}
<span ref={setRef('random_id_3')}></span>
{/* Set refs dynamically in Array.map() */}
{ foo.map( eachId => (
<div key={eachId} ref={setRef(eachId)}>Hello {eachId}</div>))}
</>
)
}
export default Example;

You have to use a separate set of hooks for each item, and this means you have to define a component for the items (or else you’re using hooks inside a loop, which isn’t allowed).
const Item = ({ text }) => {
const ref = useRef()
const [ height, setHeight ] = useState()
useLayoutEffect(() => {
setHeight( ref.current.getBoundingClientRect().height )
}, [])
return <div ref={ref}>{text}</div>
}

Related

React component not re-rendering when array prop changes

I want my App component to display keys every time keys changes. I'm doing this by passed keys as a prop of App:
import * as React from "react";
import { render } from "react-dom";
import { useState, useEffect } from "react"
let keys: string[] = [];
// This is what is supposed to happen in the real app
// document.addEventListener("keypress", (event) => {
// keys.push(event.key)
// });
setTimeout(() => {
keys.push('a');
}, 1000)
function App({ keys }: { keys: string[] }) {
let [keysState, setKeysState] = useState(keys)
useEffect(() => {
setKeysState(keys)
}, [keys])
return (
<div>
{keysState.map((key: string) => (
<li>{key}</li>
))}
</div>
);
}
const rootElement = document.getElementById("root");
render(<App keys={keys} />, rootElement);
However, App isn't re-rendering and displaying keys new value:
https://codesandbox.io/s/changing-props-on-react-root-component-forked-3mv0xf?file=/src/index.tsx
Why is this, and how to fix it?
Note: I tried: setKeysState([...keys, 'a']). That doesn't re-render App either.
Live code: https://codesandbox.io/s/changing-props-on-react-root-component-forked-3mv0xf?file=/src/index.tsx
All data that is dynamic needs to be managed by React. Put your event inside the component and update local state.
function App({ initialKeys }: { initialKeys: string[] }) {
const [keys, setKeys] = React.useState(initialKeys);
console.log(keys);
React.useEffect(() => {
const append = (e) => {
setKeys([...keys, e.key]);
};
document.addEventListener("keypress", append);
return () => {
document.removeEventListener("keypress", append);
};
}, [keys]);
return (
<div>
{keys.map((key: string, idx) => (
<li key={idx}>{key}</li>
))}
</div>
);
}
https://codesandbox.io/s/changing-props-on-react-root-component-forked-l869dd?file=/src/index.tsx
if you use the below strategy it works as you want it to work.
React cannot see state changes out of its built-in functions so it didn't track the change on your array which was out of its state scope
import * as React from "react";
import { render } from "react-dom";
import { useState, useEffect } from "react";
let keys: string[] = [];
function App(props: any) {
const [keys, oldKeysState] = useState(props.keys);
const [keysState, setKeysState] = useState(keys);
useEffect(() => {
setKeysState(keys);
}, [keys]);
// componentWillMount
useEffect(() => {
setTimeout(() => {
oldKeysState([...keys, "a"]);
}, 1000);
}, []);
return (
<div>
{keysState.map((key: string) => (
<li>{key}</li>
))}
<button onClick={() => setKeysState([...keysState, "+"])}>+</button>
</div>
);
}
const rootElement = document.getElementById("root");
render(<App keys={keys} />, rootElement);

Passing an Array from one UseState to Another

I'm currently trying to figure out how to pass an array from one useState object to another across two different components. In my first useState I have an array called imagesOriginal, which gets filled with file paths dynamically to various different images like in the following:
[
"https://source.unsplash.com/WLUHO9A_xik/900x900",
"https://source.unsplash.com/R4K8S77qtwI/900x900",
"https://source.unsplash.com/jJGc21mEh8Q/900x900"
]
In my App.js, I construct it like so.
import React, { useCallback, useState } from 'react';
import ShowImage from './ShowImage.jsx';
import DropBox from './DropBox.js';
function App() {
const [imagesOriginal, setImages] = useState([]);
const onDrop = useCallback((acceptedFiles) => {
acceptedFiles.map((file, index) => {
const reader = new FileReader();
reader.onload = function (e) {
setImages((prevState) => [
...prevState,
{ id: index, src: e.target.result },
]);
};
reader.readAsDataURL(file);
return file;
});
}, []);
return (
<div className="App">
<div class="parent">
<div>
<h3>Originals</h3>
<DropBox onDrop={onDrop} />
<ShowImage images={imagesOriginal}/>
</div>
</div>
</div>
);
}
export default App;
The main issue comese in the ShowImage.jsx, where I want to pass that array of images to another useState, as I need to use both the array and the setItems to sort the array with a new order.
import React, { useState } from 'react';
import {
DndContext,
closestCorners,
MouseSensor,
TouchSensor,
DragOverlay,
useSensor,
useSensors,
} from '#dnd-kit/core';
import "./ShowImage.css"
import {arrayMove, SortableContext} from '#dnd-kit/sortable';
import {SortablePhoto} from './SortablePhoto.jsx';
const ShowImage = ({images}) => {
const [items, setItems] = useState([]);
setItems([...images]);
const [activeId, setActiveId] = useState(null);
const sensors = useSensors(useSensor(MouseSensor), useSensor(TouchSensor));
return(
<div class="scroll">
<DndContext
sensors={sensors}
collisionDetection={closestCorners}
onDragStart={handleDragStart}
onDragOver={handleDragOver}
onDragEnd={handleDragEnd}
onDragCancel={handleDragCancel}
>
<SortableContext items={items} strategy={() => {}}>
<div columns={1}
style={{
display: "grid",
gridAutoRows: `100px`,
gridGap: 10
}}
>
{items.map((url, index) => (
<SortablePhoto key={url.src} url={url.src} index={index}/>
))}
</div>
</SortableContext>
</DndContext>
</div>
);
function handleDragStart(event) {
setActiveId(event.active.id);
}
function handleDragOver(event) {
const {active, over} = event;
if (active.id !== over.id) {
setItems((items) => {
const oldIndex = items.indexOf(active.id);
const newIndex = items.indexOf(over.id);
return arrayMove(items, oldIndex, newIndex);
});
}
}
function handleDragEnd(event) {
setActiveId(null);
}
function handleDragCancel() {
setActiveId(null);
}
};
export default ShowImage;
I've tried using the line setItems([...images]); to try and pass the new items in, and also const [items, setItems] = useState(images);, but It never seems to update the items array. I'm probably doing something really stupid, but any help would be greatly appreciated.
You can create a function in your App component that wraps your setItems state modifier and pass this function as a prop to your nested ShowImage component where you could use it to manipulate the state. This way you won't need to maintain 2 different states.
// App.js
function App() {
const [imagesOriginal, setImages] = useState([]);
const setImagesWrapper = useCallback(val => {
setImages(val);
}, [setImages]);
//...
return (
<div className="App">
<div class="parent">
<div>
<h3>Originals</h3>
<DropBox onDrop={onDrop} />
<ShowImage
images={imagesOriginal}
setImages={setImagesWrapper}
/>
</div>
</div>
</div>
);
}
export default App;
// ShowImage.js
const ShowImage = ({ images, setImages }) => {
const [activeId, setActiveId] = useState(null);
const sensors = useSensors(useSensor(MouseSensor), useSensor(TouchSensor));
// ...
};
export default ShowImage;

document.getElementById() does not find anything ReactJS

Hello I'm creating a progerss bar using ReactJS but I have a problem
My progress bar is going to be a React component and I will pass the progress value in the props
For some reason it seems like document.getElementById() does not find anything
import './MyProgressBar.css'
const MyProgressBar = (props) => {
const value = props.value
const progressbar = document.getElementById("hello");
progressbar.style.width = value + "%"
return (
<div class="progress">
<div id="hello" class="color"></div>
</div>
)
}
export default MyProgressBar
It throws an error that says "Cannot read property 'style' of null at MyProgressBar (MyProgressBar.js:9)....."
The reason for the error is that the element doesn't exist yet as of when you go looking for it. You could "fix" that with a useEffect or useLayoutEffect callback, but that wouldn't be the React approach. Your component will be called to re-render when the props change, so handle rendering in the new state directly:
const MyProgressBar = ({value}) => {
return (
<div class="progress">
<div class="color" style={{width: value + "%"}}></div>
</div>
);
};
This also has the advantage that you can have multiple MyProgressBar instances in the page at the same time.
Live Example:
const {useState, useEffect} = React;
const MyProgressBar = ({value}) => {
return (
<div className="progress">
<div className="color" style={{width: value + "%"}}></div>
</div>
);
};
const App = () => {
const [bar1, setBar1] = useState(0);
const [bar2, setBar2] = useState(0);
useEffect(() => {
const t1 = setInterval(() => {
setBar1(b1 => {
if (b1 < 100) {
++b1;
return b1;
}
clearInterval(t1);
return b1;
});
}, 200);
const t2 = setInterval(() => {
setBar2(b2 => {
if (b2 < 100) {
++b2;
return b2;
}
clearInterval(t2);
return b2;
});
}, 400);
}, []);
return <div>
<div>
Every 200ms:
<MyProgressBar value={bar1} />
</div>
<div>
Every 400ms:
<MyProgressBar value={bar2} />
</div>
</div>;
};
ReactDOM.render(<App/>, document.getElementById("root"));
.color {
background-color: blue;
height: 100%;
}
.progress {
height: 1em;
}
<div id="root"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.2/umd/react.development.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.2/umd/react-dom.development.js"></script>
This code is running before there is anything on the DOM.
You can use it inside a useEffect, running after the render.
useEffect(() => {
const value = props.value
const progressbar = document.getElementById("hello");
progressbar.style.width = value + "%";
}, [props.value]);
You can also use useRef
You document.getElementById is run before actual rendering, so it could not find any element with that Id
It's not reccomend to use document.getElementById in your Reactjs code, use useRef instead:
function App() {
const divRef = React.useRef();
React.useEffect(() => {
console.log(divRef.current);
}, []);
return (
<div class="progress">
<div ref={divRef} id="hello" class="color"></div>
</div>
);
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.2/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.2/umd/react-dom.production.min.js"></script>
<div id="root"></div>
As explained earlier document.getElementById won't work in case of react. Because it is running before actual DOM is being created.
First method is to use useEffect
import './MyProgressBar.css'
import { useEffect, useState } from 'react';
const MyProgressBar = (props) => {
const [progressbarWidth, setProgressbarWidth] = useState(0)
useEffect(() => {
const value = props.value
setProgressbarWidth(value)
}, [props.value])
return (
<div class="progress" style={{ width: `${progressbarWidth}%` }}>
<div id="hello" class="color"></div>
</div>
)
}
export default MyProgressBar
Second method is to use useRef
import './MyProgressBar.css'
import { useEffect, useRef } from 'react';
const MyProgressBar = (props) => {
const progressbarRef = useRef()
useEffect(() => {
const value = props.value
progressbarRef.current.style.width = value + "%"
}, [props.value])
return (
<div class="progress" ref={progressbarRef}>
<div id="hello" class="color"></div>
</div>
)
}
export default MyProgressBar
In react you don't have to access elements by id.
I encourage you to do something like this:
import './MyProgressBar.css'
const MyProgressBar = (props) => {
const value = props.value
return (
<div class="progress">
<div style={{width: `${value}%` }}></div>
</div>
)
}
export default MyProgressBar

map over ListItem, checkbox only to change for one row (React Native)

I'm using nativebase checkbox and I'm mapping over the ListItem, the issue is that when a user selects one row it changes the status of ALL the rows. How do I get it to only change one row and not all of them during mapping
const [status, setStatus] = useState(false);
{Object.entries(
allEquipmentList.reduce(
(byType, item) => ({
...byType,
[item.type]: [...(byType[item.type] || []), item]
}),
{}
)
).map(([type, items]) =>
items.map((item, index) => {
return (
<>
<ListItem onPress={() => setStatus(!status)}>
<CheckBox checked={status} />
<Body>
<Text>{item.name}</Text>
</Body>
</ListItem>
</>
)
})
You have only one state, so, every checkbox are changing it. You need multiple states. How: put const [status, setStatus] = useState(false); inside ListItem and reimplement the switch logic.
You should move <CheckBox checked={status} /> inside ListItem too for it to have access for your state.
Ex:
function ListItem({children}) {
const [status, setStatus] = useState(false);
return (
<ListItemInner onPress={() => setStatus(!status)}>
<CheckBox checked={status} />
<Body>
<Text>{children}</Text>
</Body>
</ListItemInner>
)
}
function SomeComponent(){
// ... your code here ...
return {Object.entries(
allEquipmentList.reduce(
(byType, item) => ({
...byType,
[item.type]: [...(byType[item.type] || []), item]
}),
{}
)
).map(([type, items]) => {
return items.map((item, index) => {
return <ListItem key={index}>{item.name}</ListItem>
}
})
}
Edit: "how to log what have been selected?"
This is the base code for log, but you still can't retrieve this information up in the tree
import { useEffect, useRef, useState } from "react";
import styled from "styled-components";
// just styles
const Label = styled.label`
display: flex;
align-items: center;
border: 1px solid #eee;
margin: 10px;
cursor: pointer;
`;
function ListItem({ children }) {
const [status, setStatus] = useState(false);
// useRef helps you to get HTML element (in this case)
// check this out: https://reactjs.org/docs/hooks-reference.html
const itemRef = useRef(null);
// useEffect allow you to see changes in your state. Just log outside could show you old states
useEffect(() => {
console.log(itemRef.current.innerText, status);
}, [status]);
return (
<Label>
<input
type="checkbox"
// let the browser handle the press, you just care about the change it made
onChange={() => setStatus(!status)}
checked={status}
/>
{/* basically a getElementById */}
<p ref={itemRef}>
<span>{children}</span>
</p>
</Label>
);
}
export default function App() {
const demoItems = ["foo", "doo", "boo"];
console.log("which items have been selected?");
return (
<div>
{demoItems.map((item, index) => (
<ListItem key={index}>{item}</ListItem>
))}
</div>
);
}
Edit 2: "how to access what was selected up in the tree? (aka, get the data)"
Here is the final code. Be aware, I don't think this is the best way of do it, but it works. Also, you should use some id for that, not the name. Use it as learning process or hard-test it
Codesandbox of it: https://codesandbox.io/s/so-map-over-listitem-checkbox-only-to-change-for-one-row-react-native-kp12p?file=/src/App.js:133-1829
import { useEffect, useRef, useState } from "react";
import styled from "styled-components";
const Label = styled.label`
display: flex;
align-items: center;
border: 1px solid #eee;
margin: 10px;
cursor: pointer;
`;
function ListItem({ selected, setSelected, children }) {
const [status, setStatus] = useState(false);
const itemRef = useRef(null);
// outter control, uses a higher state
function handleChange() {
const el = itemRef?.current?.innerText;
const wasSelected = selected.includes(el);
console.log("el ->", el);
console.log("wasSelected ->", wasSelected);
if (wasSelected) {
setSelected((s) => s.filter((item) => item !== el));
} else {
setSelected((s) => [...s, el]);
}
// if the syntax of the state is weird to you, check it: https://stackoverflow.com/questions/42038590/when-to-use-react-setstate-callback
}
// just inner control, handles the visual update
useEffect(() => {
const el = itemRef?.current?.innerText;
setStatus(selected.includes(el));
}, [selected]);
return (
<Label>
<input type="checkbox" onChange={handleChange} checked={status} />
<p ref={itemRef}>
<span>{children}</span>
</p>
</Label>
);
}
export default function App() {
const demoItems = ["foo", "doo", "boo"];
const [selected, setSelected] = useState(["foo"]);
useEffect(() => {
console.log("which items have been selected?");
console.log(selected);
}, [selected]);
return (
<div>
{demoItems.map((item, index) => (
<ListItem key={index} selected={selected} setSelected={setSelected}>
{item}
</ListItem>
))}
</div>
);
}

How to check if a div is overflowing in react functional component

I am trying to find out if a div has overflown text and show show more link if it does. I found this stackoverflow answer to check if a div is overflowing. According to this answer, I need to implement a function which can access styles of the element in question and do some checks to see if it is overflowing. How can I access the styles of an element. I tried 2 ways
1. Using ref
import React from "react";
import "./styles.css";
export default function App(props) {
const [showMore, setShowMore] = React.useState(false);
const onClick = () => {
setShowMore(!showMore);
};
const checkOverflow = () => {
const el = ref.current;
const curOverflow = el.style.overflow;
if ( !curOverflow || curOverflow === "visible" )
el.style.overflow = "hidden";
const isOverflowing = el.clientWidth < el.scrollWidth
|| el.clientHeight < el.scrollHeight;
el.style.overflow = curOverflow;
return isOverflowing;
};
const ref = React.createRef();
return (
<>
<div ref={ref} className={showMore ? "container-nowrap" : "container"}>
{props.text}
</div>
{(checkOverflow()) && <span className="link" onClick={onClick}>
{showMore ? "show less" : "show more"}
</span>}
</>
)
}
2. Using forward ref
Child component
export const App = React.forwardRef((props, ref) => {
const [showMore, setShowMore] = React.useState(false);
const onClick = () => {
setShowMore(!showMore);
};
const checkOverflow = () => {
const el = ref.current;
const curOverflow = el.style.overflow;
if (!curOverflow || curOverflow === "visible") el.style.overflow = "hidden";
const isOverflowing =
el.clientWidth < el.scrollWidth || el.clientHeight < el.scrollHeight;
el.style.overflow = curOverflow;
return isOverflowing;
};
return (
<>
<div ref={ref} className={showMore ? "container-nowrap" : "container"}>
{props.text}
</div>
{checkOverflow() && (
<span className="link" onClick={onClick}>
{showMore ? "show less" : "show more"}
</span>
)}
</>
);
});
Parent component
import React from "react";
import ReactDOM from "react-dom";
import { App } from "./App";
const rootElement = document.getElementById("root");
const ref = React.createRef();
ReactDOM.render(
<React.StrictMode>
<App
ref={ref}
text="Start editing to see some magic happen! Click show more to expand and show less to collapse the text"
/>
</React.StrictMode>,
rootElement
);
But I got the following error in both approaches - Cannot read property 'style' of null.
What am I doing wrong? How can I achieve what I want?
As Jamie Dixon suggested in the comment, I used useLayoutEffect hook to set showLink true. Here is the code
Component
import React from "react";
import "./styles.css";
export default function App(props) {
const ref = React.createRef();
const [showMore, setShowMore] = React.useState(false);
const [showLink, setShowLink] = React.useState(false);
React.useLayoutEffect(() => {
if (ref.current.clientWidth < ref.current.scrollWidth) {
setShowLink(true);
}
}, [ref]);
const onClickMore = () => {
setShowMore(!showMore);
};
return (
<div>
<div ref={ref} className={showMore ? "" : "container"}>
{props.text}
</div>
{showLink && (
<span className="link more" onClick={onClickMore}>
{showMore ? "show less" : "show more"}
</span>
)}
</div>
);
}
CSS
.container {
overflow-x: hidden;
text-overflow: ellipsis;
white-space: nowrap;
width: 200px;
}
.link {
text-decoration: underline;
cursor: pointer;
color: #0d6aa8;
}
We could create a custom hooks to know if we have overflow.
import * as React from 'react';
const useIsOverflow = (ref, isVerticalOverflow, callback) => {
const [isOverflow, setIsOverflow] = React.useState(undefined);
React.useLayoutEffect(() => {
const { current } = ref;
const { clientWidth, scrollWidth, clientHeight, scrollHeight } = current;
const trigger = () => {
const hasOverflow = isVerticalOverflow ? scrollHeight > clientHeight : scrollWidth > clientWidth;
setIsOverflow(hasOverflow);
if (callback) callback(hasOverflow);
};
if (current) {
trigger();
}
}, [callback, ref, isVerticalOverflow]);
return isOverflow;
};
export default useIsOverflow;
and just check in your component
import * as React from 'react';
import { useIsOverflow } from './useIsOverflow';
const App = () => {
const ref = React.useRef();
const isOverflow = useIsOverflow(ref);
console.log(isOverflow);
// true
return (
<div style={{ overflow: 'auto', height: '100px' }} ref={ref}>
<div style={{ height: '200px' }}>Hello React</div>
</div>
);
};
Thanks to Robin Wieruch for his awesome articles
https://www.robinwieruch.de/react-custom-hook-check-if-overflow/
Solution using TS and Hooks
Create your custom hook:
import React from 'react'
interface OverflowY {
ref: React.RefObject<HTMLDivElement>
isOverflowY: boolean
}
export const useOverflowY = (
callback?: (hasOverflow: boolean) => void
): OverflowY => {
const [isOverflowY, setIsOverflowY] = React.useState(false)
const ref = React.useRef<HTMLDivElement>(null)
React.useLayoutEffect(() => {
const { current } = ref
if (current) {
const hasOverflowY = current.scrollHeight > window.innerHeight
// RHS of assignment could be current.scrollHeight > current.clientWidth
setIsOverflowY(hasOverflowY)
callback?.(hasOverflowY)
}
}, [callback, ref])
return { ref, isOverflowY }
}
use your hook:
const { ref, isOverflowY } = useOverflowY()
//...
<Box ref={ref}>
...code
Import your files as need be and update code to your needs.

Categories