Passing a React.MutableRefObject to sibling elements - reactjs

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>
);
});

Related

React TypeScript containment parent passing props to children

I've been trying this for hours but I haven't found a satisfactory solution. I want to have this wrapper that contains some state that I can then either pass to its child or render something else.
I would like to do something like this abstract example. Is there anything along these lines that I can do?
const MyChild = (props:{state:boolean}) => {
return <Text>`the state is ${props.state}`</Text>
}
const StateWrapper = ({children}:{children:React.ReactNode}) => {
const hookState:boolean|null = useHookState()
if (null) return <Loading />
return {children} <-- with {state:boolean}
}
const App = () => {
return <StateWrapper><MyChild /><StateWrapper>
}
A common pattern for this kind of problem is the "Render Props" approach. The "state wrapper" object takes a prop that passes its data to something else to render. This way you don't have to do any weird changing or copying of state data, and names don't necessarily have to align perfectly, making it easy to swap in other components in the future.
const MyChild = (props: {state: boolean}) => {
return <Text>`the state is ${props.state}`</Text>
}
const StateWrapper = ({children}:{children: (state: boolean) => React.ReactNode}) => {
const hookState:boolean|null = useHookState()
if (null) return <Loading />
return children(state);
}
const App = () => {
return (
<StateWrapper>
{(state) => (<MyChild state={state}/>)}
</StateWrapper>
);
}
See more: https://reactjs.org/docs/render-props.html
3 types of wrapper with ref and added props in typescript:
Sandbox Demo:https://codesandbox.io/s/wrapper-2kn8oy?file=/src/Wrapper.tsx
import React, { cloneElement, forwardRef, useRef, useState } from "react";
interface MyChild extends React.ReactElement {
ref?:React.Ref<HTMLDivElement>|undefined
}
interface MyProps {
children:MyChild|MyChild[],
added:string,
}
const Wrapper1 = ({ children, added }: MyProps) => {
const e =
Array.isArray(children) ?
children.map((child) => {
return cloneElement(child, { added: added })
}) :
cloneElement(children)
return <>
{e}
</>
}
const Wrapper2 = ({ children, added }:MyProps) => {
const e =
Array.isArray(children) ?
children.map((child) => {
return <child.type {...child.props} added={added} ref={child.ref} />
}) :
children?<children.type {...children.props} added={added} ref={children.ref}/>:null
//console.log("2:",e)
if(!Array.isArray(children))console.log(children.ref)
return <>
{e}
</>
}
const Wrapper3 = ({ children, added }:{children:any,added:any}) => {
return <>
{children(added)}
</>
}
const Mydiv = forwardRef((props: any, ref?: any) =>
<div ref={ref}>Origin:{props.message},Added:{props.added ?? "None"}</div>
)
const Container = () => {
const ref1 = useRef<HTMLDivElement>(null)
const ref2 = useRef<HTMLDivElement>(null)
const ref3 = useRef<HTMLDivElement>(null)
const [refresh, setRefresh] = useState(1)
const setColor = () => {
console.log("ref1:", ref1.current, "ref2:", ref2.current, "ref3:", ref3.current)
if (ref1.current) ref1.current.style.color = "red"
if (ref2.current) ref2.current.style.color = "red"
if (ref3.current) ref3.current.style.color = "red"
}
return <>
<button onClick={setColor}>Change Color</button>
<button onClick={() => setRefresh((n) => n + 1)}>Refresh page</button>
<div>{`state:${refresh}`}---state:{refresh}</div>
<Wrapper1 added="Wrapper1 added">
<Mydiv ref={ref1} message={`MyDiv 1 with Ref,parent state:${refresh}`} />
<Mydiv message={`MyDiv 1 without ref,parent state:${refresh}`} />
</Wrapper1>
<Wrapper2 added="Wrapp2 added">
<Mydiv ref={ref2} message={`MyDiv 2 with Ref,parent state:${refresh}`} />
<Mydiv message={`MyDiv 2 without ref,parent state:${refresh}`} />
</Wrapper2>
<Wrapper3 added="Wrapp3 added">
{(added: any) => (<>
<Mydiv ref={ref3} message={`MyDiv 3 with Ref,parent state:${refresh}`} added={added} />
<Mydiv message={`MyDiv 3 without Ref,parent state:${refresh}`} added={added} />
</>
)}
</Wrapper3>
</>
}
export default Container

React state doesnt update, drilling seems ok though

I've got a modal that I want to be able to auto-shut itself using a drilled function. The console.log does work, but the state isn't actually updating. What am I doing wrong? Triggering the state via the dev tools works fine, so it's not the state itself. Is drilling within a component the problem?
index.js:
export default function Home() {
const [modalOpen, setModalOpen] = useState(false)
const handleModalOpen = () => {
console.log ("Setting modal to true")
setModalOpen (true)
}
const handleModalClose = () => {
console.log ("Setting modal to false")
setModalOpen (false)
}
// all the normal app body
{modalOpen ?
(<Modal handleModalClose={handleModalClose} height='30vh'>
<h4>Thank you for your contact request.</h4>
<h4>Keep in mind that this is only a demo website, not an actual business.</h4>
</Modal>): null}
</div>
)
}
Modal.js:
import { createPortal } from "react-dom";
import { useEffect, useState } from "react";
import styles from '../styles/Modal.module.css'
const Backdrop = (props) => {
return <div onClick={() => props.handleModalClose()} className={styles.backdrop} />
}
const Message = (props) => {
let width = '70vw'
let height = '80vh'
if (props.width) width = props.width
if (props.height) height = props.height
return (
<div style={{ width: width, height: height }} className={styles.message}>
{props.children}
</div>
)
}
const Modal = (props) => {
const [backdropDiv, setBackdropDiv] = useState(null)
const [modalDiv, setModalDiv] = useState(null)
useEffect(() => {
if (typeof (window) !== undefined) {
let backdropDiv = document.getElementById('backdrop')
setBackdropDiv(backdropDiv)
let modalDiv = document.getElementById('modal')
setModalDiv(modalDiv)
}
}, [])
return (
<>
{backdropDiv !== null && modalDiv !== null ? (
<>
{createPortal(<Backdrop handleModalClose = {props.handleModalClose} />, backdropDiv)}
{createPortal(<Message children={props.children} width={props.width} height={props.height} />, modalDiv)}
</>
) : null
}
</>
)
}
export default Modal

How to add types to React Table IndeterminateCheckbox method

I'm really a beginner at typescript world and, I'm currently using React Table library that has no types by default on documentation.
So, I would like to ask your help to add the types to IndeterminateCheckbox method.
const IndeterminateCheckbox = React.forwardRef(
({ indeterminate, ...rest }, ref) => {
const defaultRef = React.useRef()
const resolvedRef = ref || defaultRef
React.useEffect(() => {
resolvedRef.current.indeterminate = indeterminate
}, [resolvedRef, indeterminate])
return (
<>
<input type="checkbox" ref={resolvedRef} {...rest} />
</>
)
}
)
Here is the link to sandbox from React Table docs:
https://codesandbox.io/s/github/tannerlinsley/react-table/tree/master/examples/row-selection-and-pagination?from-embed=&file=/src/App.js:613-1010
My second question is: Where can I find the types and maybe add them by myself?
1. Create #types/react.d.ts
/* eslint-disable */
declare namespace React {
export default interface RefObject<T> {
current: T | null;
}
}
2. The input component
/* eslint-disable no-param-reassign */
import React, { forwardRef, useEffect, useRef } from 'react';
interface IIndeterminateInputProps {
indeterminate?: boolean;
name: string;
}
const useCombinedRefs = (
...refs: Array<React.Ref<HTMLInputElement> | React.MutableRefObject<null>>
): React.MutableRefObject<HTMLInputElement | null> => {
const targetRef = useRef(null);
useEffect(() => {
refs.forEach(
(ref: React.Ref<HTMLInputElement> | React.MutableRefObject<null>) => {
if (!ref) return;
if (typeof ref === 'function') {
ref(targetRef.current);
} else {
ref.current = targetRef.current;
}
},
);
}, [refs]);
return targetRef;
};
const IndeterminateCheckbox = forwardRef<
HTMLInputElement,
IIndeterminateInputProps
>(({ indeterminate, ...rest }, ref: React.Ref<HTMLInputElement>) => {
const defaultRef = useRef(null);
const combinedRef = useCombinedRefs(ref, defaultRef);
useEffect(() => {
if (combinedRef?.current) {
combinedRef.current.indeterminate = indeterminate ?? false;
}
}, [combinedRef, indeterminate]);
return (
<>
<input type="checkbox" ref={combinedRef} {...rest} />
</>
);
});
export default IndeterminateCheckbox;
I just want to put code here so it is visible to everyone having issues with it. Solution is from the thread from Megha's comment:
import React from "react";
type Props = {
indeterminate?: boolean;
};
const TableCheckBox: React.ForwardRefRenderFunction<HTMLInputElement, Props> = ({ indeterminate = false, ...rest }, ref) => {
const defaultRef = React.useRef<HTMLInputElement>();
const resolvedRef = (ref || defaultRef) as React.MutableRefObject<HTMLInputElement>;
React.useEffect(() => {
resolvedRef.current.indeterminate = indeterminate;
}, [resolvedRef, indeterminate]);
return (
<>
<input type="checkbox" ref={resolvedRef} {...rest} />
</>
);
};
export default React.forwardRef(TableCheckBox);
Link: https://github.com/TanStack/table/discussions/1989#discussioncomment-4388612

How to avoid re-render of React Hooks

Hi I am new developer at ReactJs. I have a question about Hooks. I know there are so many answers about this quest but I did not integrate that solutions to my project. I have parent and child component. I am passing value from child to parent. When I try to send a data via react callback function, parent is re-rendering while taking item and so I lost my selected value on Child. How can I protect that problems ? Could you any advice to me ?
Parent Component :
import React,{useCallback,useState} from 'react'
const Dropable = ({item}) => {
const [list, setList] = useState(item)
const [selectedItems, setSelectedItems] = useState<Array<any>>([])
const handleSelectedItems = useCallback( (data: any) => {
if (data.type === "player") {
if (selectedItems.length === 0) {
const arr = selectedItems
arr.push(data)
setSelectedItems(arr)
}
}, [selectedItems],
)
return (
{list.map((data: any, index: any) => {
return (
<>
{
<div onClick={() => handleSelectedItems(index, data)}>
<Detailed
key={uuid()}
data={data}
dataIndex={index}
/>
</div>
}
</>
)
})}
)
}
export default Dropable;
Child Component:
import React,{useState} from 'react'
const Detailed : React.FC<IProps> = (props) {
const [selected, setSelected] = useState(false)
const handleSelect = () => {
if (selected) {
setSelected(false)
}
else {
setSelected(true)
}
}
return (
<div onClick={handleSelect} className="detail" style={selected?{border: " 1px solid #5c6ef5"}: {}}>
</div>
)
}
export default Detailed;
Hi Instead of maintaining the boolean value in the child to see if the item is selected or not you can pass that value from parent to child.
So the parent component:
const Dropable = ({ item }) => {
const [list, setList] = useState(item)
const [selectedItems, setSelectedItems] = useState<Array<any>>([])
const handleSelectedItems = useCallback((data: any) => {
if (data.type === "player") {
if (selectedItems.length === 0) {
const arr = selectedItems
arr.push(data)
setSelectedItems(arr)
}
}, [selectedItems],
)
return (
{
list.map((data: any, index: any) => {
return (
<>
{
<div onClick={() => handleSelectedItems(index, data)}>
<Detailed
key={uuid()}
data={data}
dataIndex={index}
selected={selectedItems[0][ANY_KEY] === data[ANY_key]}
/>
</div>
}
</>
)
})
}
)
}
export default Dropable;
And your child component:
There is no need to use useState to set the value coz we are doing the same work in the parent component.
import React,{useState} from 'react'
const Detailed : React.FC<IProps> = (props) {
return (
<div className="detail" style={props.selected?{border: " 1px solid
#5c6ef5"}: {}}>
</div>
)
}
export default Detailed;
One more tip: It is not a good practice to send data from child component to parent.
You can simply pass the selected value from parent to child component and initiate the selected state using that props.
const [selected, setSelected] = useState(props.selected)
Then every time the parent updates, the child's selected state will not be null.
You can pass null as initial value for selected via props. So, you will call the child component like this,
import React,{useCallback,useState} from 'react'
const Dropable = ({item}) => {
const [list, setList] = useState(item)
const [selectedItems, setSelectedItems] = useState<Array<any>>([])
const handleSelectedItems = useCallback( (data: any) => {
if (data.type === "player") {
if (selectedItems.length === 0) {
const arr = selectedItems
arr.push(data)
setSelectedItems(arr)
}
}, [selectedItems],
)
return (
{list.map((data: any, index: any) => {
const isSelected = selectedItems.findIndex(item => item.[some_unique_attribute] === data.[some_unique_attribute]) > -1;
return (
<>
{
<div onClick={() => handleSelectedItems(index, data)}>
<Detailed
key={uuid()}
data={data}
dataIndex={index}
selected={isSelected}
/>
</div>
}
</>
)
})}
)
}
The only reason that your child loses its state when the parent re-renders is because you are assigning a new key to the Child component while mapping
Assigning a key that is unique but also remains same across re-renders will prevent this problem from happening
If the key to a component changes, its not re-rendered but re-mounted and hence state values are reset
Either you can use a unique value as key from data or alternative use index if there is no unique key in data. However you must try to have atleast 1 field in data which can be used to uniquely identify it
return (
{
list.map((data: any, index: any) => {
return (
<>
{
<div onClick={() => handleSelectedItems(index, data)}>
<Detailed
key={data.id} // if no id field. You can assign index
data={data}
dataIndex={index}
selected={selectedItems[0][ANY_KEY] === data[ANY_key]}
/>
</div>
}
</>
)
})
}
)

React compute children's total width using useRef

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;

Resources