React compute children's total width using useRef - reactjs

How do I compute the children's total width using React's useRef? What I want to achieve is to access each child's property including ref. Note that each Child's component has different width. I have a codesandbox here.
import React from "react";
const ComputeWidth = ({ children }) => {
let totalWidth = 0;
const newChildren = React.Children.map(children, element => {
const newProps = {
...element.props,
additionalProp: 1234
};
// I WANT TO ACCESS CHILD'S WIDTH HERE
// element.ref is null
// totalWidth += element.ref.current.offsetWidth???
return React.cloneElement(element, newProps);
});
return <div>{newChildren}</div>;
};
export const Child = ({ label }) => label;
export default ComputeWidth;

I was able to answer this. However, I am not sure if passing a ref to a prop is a good approach. Codesandbox here.
import React, { useState, useRef, useEffect } from "react";
const ComputeWidth = ({ children }) => {
const [totalWidth, setTotalWidth] = useState(0);
const els = React.Children.map(children, useRef);
const newChildren = React.Children.map(children, (element, i) => {
const newProps = {
...element.props,
additionalProp: 1234,
el: els[i]
};
return <element.type ref={els[i]} {...newProps} />;
});
useEffect(() => {
setTotalWidth(
newChildren.reduce(
(pv, cv) => pv.ref.current.offsetWidth + cv.ref.current.offsetWidth
)
);
}, []);
return (
<div>
{newChildren}
<div>Width is {totalWidth}</div>
</div>
);
};
export const Child = ({ label, el }) => (
<div ref={el} style={{ display: "inline" }}>
{label}
</div>
);
export default ComputeWidth;

Related

Higher Order Component to observe Visibility: React?

I have created a higher order component as shown below:
import React from 'react';
interface IVisibility {
Component: JSX.Element;
visibilityThreshold?: number;
onVisibleCallback?: () => void;
}
const VisibilityHandler = ({
Component,
visibilityThreshold,
onVisibleCallback
}: IVisibility) => {
const ref = React.useRef(null);
React.useEffect(() => {
const componentObserver = new IntersectionObserver(
(entries) => {
const [entry] = entries;
if (entry.isIntersecting) {
onVisibleCallback ? onVisibleCallback() : null;
}
},
{
rootMargin: '0px',
threshold: visibilityThreshold ?? 0
}
);
const current = ref.current;
if (current) componentObserver.observe(current);
return () => {
componentObserver.disconnect();
};
}, [visibilityThreshold, onVisibleCallback]);
return <section ref={ref}>{Component}</section>;
};
export default VisibilityHandler;
And use it like this:
<VisibilityHandler Component={<div>Hello World</div>} />
However this wraps every component into a section which I don't want. I tried using React.Fragment but that doesn't let you pass ref to track the component. Is there a better way to re-create this HOC in order to incorporate visibility tracking without wrapping it in additional div or section?
You can use
function as a children
React.cloneElement
Function as a children
<VisibilityHandler Component={({ ref }) => <div ref={ref}>Hello world</div>} />
You have to change you HOC code
- return <section ref={ref}>{Component}</section>;
+ return Component({ ref });
React.cloneElement
Documentation
your case
- return <section ref={ref}>{Component}</section>;
+ return React.cloneElement(Component, { ref });
But I highly recommend use hook (packages) instead of HOC.
react-use: useIntersection
react-intersection-observer
I found a really neat way to do so like this:
import React from 'react';
interface IVisibility {
Component: JSX.Element;
visibilityThreshold?: number;
onVisibleCallback?: () => void;
}
const VisibilityHandler = ({
Component,
visibilityThreshold,
onVisibleCallback
}: IVisibility): JSX.Element => {
const ref = React.useRef(null);
React.useEffect(() => {
const componentObserver = new IntersectionObserver(
(entries) => {
const [entry] = entries;
if (entry.isIntersecting) {
onVisibleCallback ? onVisibleCallback() : null;
}
},
{
rootMargin: '0px',
threshold: visibilityThreshold ?? 0
}
);
const current = ref.current;
if (current) componentObserver.observe(current);
return () => {
componentObserver.disconnect();
};
}, [visibilityThreshold, onVisibleCallback]);
return <Component.type {...Component.props} ref={ref} />;
};
export default VisibilityHandler;

Passing a React.MutableRefObject to sibling elements

I'm sure I'm explaining this wrong but I'm trying to pass a reference to an div that may or may not be there and read that reference in a sibling component.
Let me explain, (test code in it's entirety at the end):
I define a Overlay component that takes a React.MutableRefObject<HTMLDivElement | null> property. It then sets it's returned styled component to this ref. Though this is tricky as I'm only returning the styled div when there are items to display:
return (
overlayItems.length > 0
? (
<StyledOverlay ref={ref}>
{overlayItems && overlayItems.map(i => <p>{i}</p>)}
</StyledOverlay>
)
: (
<div></div>
)
)
I then am trying to read this ref in a sibling component where I simply check if ref.current == null with the actual const refVariable = useRef<HTMLDivElement | null>(null); call in the parent component and refVariablebeing passed into both components.
However, even with items in the overlayItems array, refVariable.current is always equal to null. I'm not sure if this is a side effect of using styled-Components or if this is because I'm passing the reference incorrectly or something else entirely. Can anyone point out my mistake?
(Oh and to add to the matter, I'm using TypeScript)
Full code
import React, { createContext, FC, useCallback, useContext, useRef, useState } from 'react';
import styled from 'styled-components';
export const RefTest = () => {
const refVariable = useRef<HTMLDivElement | null>(null);
return (
<AppState>
<RefReader ref={refVariable} />
<Overlay ref={refVariable} />
</AppState>
)
}
type AppContextValue = {
overlayItems: string[];
addOverlayItem: (newItem: string) => void,
};
const AppContext = createContext<AppContextValue>(undefined!);
const AppState: FC = ({ children }) => {
const [overlayItems, setOverlayItems] = useState<string[]>([])
const addOverlayItem = (newItem: string) => {
setOverlayItems(prev => [...prev, newItem]);
}
return (
<AppContext.Provider value={{
overlayItems,
addOverlayItem,
}} >
{children}
</AppContext.Provider>
)
}
const StyledOverlay = styled.div`
height: 100vh;
width: 100vw;
position: absolute;
top: 0;
left: 0;
`;
type RefProp = {
ref: React.MutableRefObject<HTMLDivElement | null>
}
const Overlay: FC<RefProp> = ({ ref }) => {
const { overlayItems } = useContext(AppContext);
return (
overlayItems.length > 0
? (
<StyledOverlay ref={ref}>
{overlayItems && overlayItems.map(i => <p>{i}</p>)}
</StyledOverlay>
)
: (
<div></div>
)
)
}
const RefReader: FC<RefProp> = ({ ref }) => {
const { addOverlayItem, overlayItems } = useContext(AppContext);
const [result, setResult] = useState("");
const add = () => {
addOverlayItem(`${overlayItems.length + 1} - an item`)
}
const check = () => {
setResult(`the ref current has a current reference: ${!!ref.current}`);
}
return (
<div>
<p>{result}</p>
<button onClick={add} >Add Overlay Item</button>
<button onClick={check} >Check Ref Current</button>
</div>
);
}
The resolution to this ended up being to wrap the two sub components with React.forwardRef(). This caused some havoc with TypeScript trying to understand the reference though and I had to force the type
const RefReader = forwardRef<HTMLDivElement, {}>((_, ref) => {
const { addOverlayItem, overlayItems } = useContext(AppContext);
const [result, setResult] = useState("");
const add = () => {
addOverlayItem(`${overlayItems.length + 1} - an item`)
}
const check = () => {
setResult(`the ref current has a current reference: ${!!(ref as MutableRefObject<HTMLDivElement>)?.current}`);
}
return (
<div>
<p>{result}</p>
<button onClick={add} >Add Overlay Item</button>
<button onClick={check} >Check Ref Current</button>
</div>
);
});

passing object using context and doing iteration with map

This is a simple question but I couldn't reach the final result after a lot of attempts. The problem is that I want to pass an object in context and use it in another file. And then do an iteration and create a specific element for each value.
App.jsx
const [activities, setActivity] = useState([
{
key: Math.random() * Math.random(),
name: 'Hello',
}
]);
const inputValue = useRef(null);
const addActivity = () => {
const activity = {
key: Math.random() * Math.random(),
name: inputValue.current.value,
};
setActivity(activities.concat(activity));
};
const value = {
// I want to pass this parameter - only activities has problem (Activity.jsx <h1>)
// I can't achieve activities.name in Activity.jsx
activities: [...activities],
functions: {
addActivity: addActivity
},
ref: {
inputValue: inputValue
}
};
<Context.Provider
value={value}
>
Context.js
export const Context = createContext();
Activity.jsx
const { activities, functions, ref } = useContext(Context);
return (
<section className="activity-container">
<input type="text" ref={ref.inputValue} />
<button onClick={functions.addActivity}>add!</button>
{
activities.map(activity => (
<h1>activity.name</h1>
))
}
</section>
);
I believe this is what you want:
// Sharing data through context
Context file:
// Context.js
import React, { useState, useRef, createContext } from "react";
export const DataContext = createContext();
const getRandom = () => Math.random() * Math.random();
const defaultValue = {
key: getRandom(),
name: "Hello"
};
const ContextProvider = ({ children }) => {
const [activities, setActivity] = useState([defaultValue]);
const inputValue = useRef(null);
const addActivity = () => {
const activity = {
key: getRandom(),
name: inputValue.current.value
};
setActivity([...activities, activity]);
};
const value = {
activities: [...activities],
functions: { addActivity },
ref: { inputValue }
};
return <DataContext.Provider value={value}>{children}</DataContext.Provider>;
};
export default ContextProvider;
Hook to read from context:
// useDataContext
import { useContext } from "react";
import { DataContext } from "./Context";
const useDataContext = () => {
const contextValue = useContext(DataContext);
return contextValue;
};
export default useDataContext;
Child Element where you want to receive the value from context:
// Child.js
import React from "react";
import useDataContext from "./useDataContext";
const Child = () => {
const data = useDataContext();
return (
<>
{data.activities.map((val, idx) => (
<div key={idx}>Name is {val.name}</div>
))}
</>
);
};
export default Child;
And the App container:
// App.js
import Child from "./Child";
import ContextProvider from "./Context";
export default function App() {
return (
<div className="App">
<ContextProvider>
<Child />
</ContextProvider>
</div>
);
}
I've created a sandbox for you to test.
You should make sure that the Activity.jsx component is wrapped with context provider, to get the proper value from the context.
I tried in this codesandbox, and it's working properly. You can refer to this and check what you are missing.

Getting undefined while accessing Context values

I keep on getting undefined while trying to access values from the the component.Here is my Provider file content :
import React from "react";
import { FlyToInterpolator } from "react-map-gl";
export const MapContext = React.createContext();
export function MapProvider(props) {
const [viewport, setViewport] = React.useState(INITIAL_STATE);
const onLoad = () => {
setViewport(DRC_MAP);
};
return (
<MapContext.Provider
value={{
viewport,
setViewport,
onLoad
}}
{...props}
/>
);
}
export const { Consumer: MapConsumer } = MapContext;
export const withMap = Component => props => {
return (
<MapConsumer>{value => <Component map={value} {...props} />}</MapConsumer>
);
};
// this is what state gets initialised as
const INITIAL_STATE = {
height: "100vh",
width: "100%",
longitude: 23.071374,
latitude: -3.6116245,
zoom: 1.33
};
const DRC_MAP = {
longitude: 23.656,
latitude: -2.88,
zoom: 4,
transitionDuration: 3000,
transitionInterpolator: new FlyToInterpolator(),
transitionEasing: t => t * (2 - t)
};
So when i try to use the viewport ot any other values defined i get undefined.Here is my Map component that is using the above code.
import React, { useContext } from "react";
import ReactMapGL from "react-map-gl";
import { MapContext } from "./contexts/MapProvider";
const MAPBOX_TOKEN ="secret"
const mapStyle = "mapbox://styles/jlmbaka/cjvf1uy761fo41fp8ksoil15x";
export default function Map() {
const { viewport, setViewport, onLoad } = useContext(MapContext);
return (
<ReactMapGL
mapboxApiAccessToken={MAPBOX_TOKEN}
mapStyle={mapStyle}
onViewportChange={nextViewport => setViewport(nextViewport)}
onLoad={onLoad}
ref={ref => (window.mapRef = ref && ref.getMap())}
{...viewport}
/>
);
}
I've read several problems which are similar to mine but,none of them are adapted for my case.Here they are :
Context value undefined in React
React context state property is undefined
You made a Context.Provider:
export function MapProvider({ children, ...props }) {
const [viewport, setViewport] = React.useState(INITIAL_STATE);
const onLoad = () => {
setViewport(DRC_MAP);
};
return (
<MapContext.Provider
value={{
viewport,
setViewport,
onLoad
}}
{...props}
>
{children} // <-- Children are consumers
</MapContext.Provider>
);
}
But you didn't consume the context:
// Somewhere in the code you need to consume its context
function Consumer() {
return (
<MapProvider>
<Map />
</MapProvider>
);
}
And then useContext will be valid:
export default function Map() {
// Child of MapContext.Provider,
// so it can consume the context.
const { viewport, setViewport, onLoad } = useContext(MapContext);
...
}

Can a React portal be used in a Stateless Functional Component (SFC)?

I have used ReactDOM.createPortal inside the render method of a stateful component like so:
class MyComponent extends Component {
...
render() {
return (
<Wrapper>
{ReactDOM.createPortal(<FOO />, 'dom-location')}
</Wrapper>
)
}
}
... but can it also be used by a stateless (functional) component?
Will chime in with an option where you dont want to manually update your index.html and add extra markup, this snippet will dynamically create a div for you, then insert the children.
export const Portal = ({ children, className = 'root-portal', el = 'div' }) => {
const [container] = React.useState(() => {
// This will be executed only on the initial render
// https://reactjs.org/docs/hooks-reference.html#lazy-initial-state
return document.createElement(el);
});
React.useEffect(() => {
container.classList.add(className)
document.body.appendChild(container)
return () => {
document.body.removeChild(container)
}
}, [])
return ReactDOM.createPortal(children, container)
}
It can be done like this for a fixed component:
const MyComponent = () => ReactDOM.createPortal(<FOO/>, 'dom-location')
or, to make the function more flexible, by passing a component prop:
const MyComponent = ({ component }) => ReactDOM.createPortal(component, 'dom-location')
can it also be used by a stateless (functional) component
?
yes.
const Modal = (props) => {
const modalRoot = document.getElementById('myEle');
return ReactDOM.createPortal(props.children, modalRoot,);
}
Inside render :
render() {
const modal = this.state.showModal ? (
<Modal>
<Hello/>
</Modal>
) : null;
return (
<div>
<div id="myEle">
</div>
</div>
);
}
Working codesandbox#demo
TSX version based on #Samuel's answer (React 17, TS 4.1):
// portal.tsx
import * as React from 'react'
import * as ReactDOM from 'react-dom'
interface IProps {
className? : string
el? : string
children : React.ReactNode
}
/**
* React portal based on https://stackoverflow.com/a/59154364
* #param children Child elements
* #param className CSS classname
* #param el HTML element to create. default: div
*/
const Portal : React.FC<IProps> = ( { children, className, el = 'div' } : IProps ) => {
const [container] = React.useState(document.createElement(el))
if ( className )
container.classList.add(className)
React.useEffect(() => {
document.body.appendChild(container)
return () => {
document.body.removeChild(container)
}
}, [])
return ReactDOM.createPortal(children, container)
}
export default Portal
IMPORTANT useRef/useState to prevent bugs
It's important that you use useState or useRef to store the element you created via document.createElement because otherwise it gets recreated on every re-render
//This div with id of "overlay-portal" needs to be added to your index.html or for next.js _document.tsx
const modalRoot = document.getElementById("overlay-portal")!;
//we use useRef here to only initialize el once and not recreate it on every rerender, which would cause bugs
const el = useRef(document.createElement("div"));
useEffect(() => {
modalRoot.appendChild(el.current);
return () => {
modalRoot.removeChild(el.current);
};
}, []);
return ReactDOM.createPortal(
<div
onClick={onOutSideClick}
ref={overlayRef}
className={classes.overlay}
>
<div ref={imageRowRef} className={classes.fullScreenImageRow}>
{renderImages()}
</div>
<button onClick={onClose} className={classes.closeButton}>
<Image width={25} height={25} src="/app/close-white.svg" />
</button>
</div>,
el.current
);
Yes, according to docs the main requirements are:
The first argument (child) is any renderable React child, such as an element, string, or fragment. The second argument (container) is a DOM element.
In case of stateless component you can pass element via props and render it via portal.
Hope it will helps.
Portal with SSR (NextJS)
If you are trying to use any of the above with SSR (for example NextJS) you may run into difficulty.
The following should get you what you need. This methods allows for passing in an id/selector to use for the portal which can be helpful in some cases, otherwise it creates a default using __ROOT_PORTAL__.
If it can't find the selector then it will create and attach a div.
NOTE: you could also statically add a div and specify a known id in pages/_document.tsx (or .jsx) if again using NextJS. Pass in that id and it will attempt to find and use it.
import { PropsWithChildren, useEffect, useState, useRef } from 'react';
import { createPortal } from 'react-dom';
export interface IPortal {
selector?: string;
}
const Portal = (props: PropsWithChildren<IPortal>) => {
props = {
selector: '__ROOT_PORTAL__',
...props
};
const { selector, children } = props;
const ref = useRef<Element>()
const [mounted, setMounted] = useState(false);
const selectorPrefixed = '#' + selector.replace(/^#/, '');
useEffect(() => {
ref.current = document.querySelector(selectorPrefixed);
if (!ref.current) {
const div = document.createElement('div');
div.setAttribute('id', selector);
document.body.appendChild(div);
ref.current = div;
}
setMounted(true);
}, [selector]);
return mounted ? createPortal(children, ref.current) : null;
};
export default Portal;
Usage
The below is a quickie example of using the portal. It does NOT take into account position etc. Just something simple to show you usage. Sky is limit from there :)
import React, { useState, CSSProperties } from 'react';
import Portal from './path/to/portal'; // Path to above
const modalStyle: CSSProperties = {
padding: '3rem',
backgroundColor: '#eee',
margin: '0 auto',
width: 400
};
const Home = () => {
const [visible, setVisible] = useState(false);
return (
<>
<p>Hello World <a href="#" onClick={() => setVisible(true)}>Show Modal</a></p>
<Portal>
{visible ? <div style={modalStyle}>Hello Modal! <a href="#" onClick={() => setVisible(false)}>Close</a></div> : null}
</Portal>
</>
);
};
export default Home;
const X = ({ children }) => ReactDOM.createPortal(children, 'dom-location')
Sharing my solution:
// PortalWrapperModal.js
import React, { useRef, useEffect } from 'react';
import ReactDOM from 'react-dom';
import $ from 'jquery';
const PortalWrapperModal = ({
children,
onHide,
backdrop = 'static',
focus = true,
keyboard = false,
}) => {
const portalRef = useRef(null);
const handleClose = (e) => {
if (e) e.preventDefault();
if (portalRef.current) $(portalRef.current).modal('hide');
};
useEffect(() => {
if (portalRef.current) {
$(portalRef.current).modal({ backdrop, focus, keyboard });
$(portalRef.current).modal('show');
$(portalRef.current).on('hidden.bs.modal', onHide);
}
}, [onHide, backdrop, focus, keyboard]);
return ReactDOM.createPortal(
<>{children(portalRef, handleClose)}</>,
document.getElementById('modal-root')
);
};
export { PortalWrapperModal };

Resources