React cloneElement not passing properties - reactjs

In one of my projects I am trying to use a single child of my component as a "template" to render a set of products. I am cloning the child like so
useEffect(() => {
if (!children) {
return;
}
setTemplate(React.Children.only(children));
}, [children, setTemplate]);
useEffect(() => {
if (loading || !products || !template) {
return;
}
const rc = [];
products.forEach((p, i) => {
rc.push(
React.cloneElement(template, {
product: p,
key: i,
})
);
});
setRenderedChildren(rc);
}, [products, loading, template, setRenderedChildren]);
When I render this, the clones are created, however the properties never arrive to the underlying component.
Can anyone tell me what I am doing wrong?

It turns out, that the child component had some bugs and hence the properties defined in cloneElement are never assigned to my expected Component.

Related

React ForwardRef: Property 'current' does not exist on type 'ForwardedRef<HTMLElement>'

I am trying to create a component that will track the vertical scroll. The catch is – the actual scroll container is not easily predictable (in this specific case it is neither window, document nor body – it is div#__next, due to CSS overflow rules).
I want to keep the component flexible and self-contained. So I've created a ref with DOM selector as an argument. I know it is far from idiomatic (to say the least), but it suprisingly seems to be working:
// Parent component
import { useRef } from "react"
const Article = (props) => {
const scrollContainerRef = useRef<HTMLElement | null>(
document.querySelector("#__next") // <-- the scroll container reference
)
return (
<SomeContent>
<ScrollToTop treshold={640} ref={scrollContainerRef} />
</SomeContent>
)
// ScrollToTop
const ScrollToTop = forwardRef(
({ treshold }, ref) => {
const [visible, setVisible] = useState(false)
useEffect(() => {
if (ref?.current) {
ref.current.addEventListener("scroll", throttle(toggleVisible, 300))
return () => {
ref.current.removeEventListener("scroll", throttle(toggleVisible, 300))
}
}
}, [])
// …
So what's the problem? the current one is Typescript. I've spent hours trying to get the types right, but to no avail. The parent component is red squigly lines free (unless I pass globalThis, which seems to work at least in CodeSandbox), but the ScrollToTop is compaining whenever I am accessing current property:
Property 'current' does not exist on type 'ForwardedRef<HTMLElement>'.
I've tried to use React.MutableRefObject<HTMLElement | null /* or other T's */>, both in parent and in child, but it didn't help.
Any ideas how to get the types to match? Or is this a silly idea from the beginning?
CodeSandbox demo
Refs might be objects with a .current property, but they might also be functions. So you can't assume that a forwarded ref has a .current property.
I think it's a mistake to use forwardRef at all here. The purpose of forwardRef is to allow a parent component to get access to an element in a child component. But instead, the parent is the one finding the element, and then you're passing it to the child for it to use. I would use a regular state and prop for that:
const Article = (props) => {
const [scrollContainer, setScrollContainer] = useState<HTMLElement | null>(() => {
return document.querySelector("#__next");
});
return (
<SomeContent>
<ScrollToTop treshold={640} scrollContainer={scrollContainer} />
</SomeContent>
)
interface ScrollToTopProps {
treshold: number;
scrollContainer: HTMLElement | null;
}
const ScrollToTop = ({ treshold, scrollContainer }: ScrollToTopProps) => {
const [visible, setVisible] = useState(false);
useEffect(() => {
if (scrollContainer) {
const toggle = throttle(toggleVisible, 300);
scrollContainer.addEventListener("scroll", toggle);
return () => {
scrollContainer.removeEventListener("scroll", toggle);
}
}
}, [scrollContainer]);
// ...
}

React Hooks - keep arguments reference in state

I created a hook to use a confirm dialog, this hook provides the properties to the component to use them like this:
const { setIsDialogOpen, dialogProps } = useConfirmDialog({
title: "Are you sure you want to delete this group?",
text: "This process is not reversible.",
buttons: {
confirm: {
onPress: onDeleteGroup,
},
},
width: "360px",
});
<ConfirmDialog {...dialogProps} />
This works fine, but also I want to give the option to change these properties whenever is needed without declaring extra states in the component where is used and in order to achieve this what I did was to save these properties in a state inside the hook and this way provide another function to change them if needed before showing the dialog:
interface IState {
isDialogOpen: boolean;
dialogProps: TDialogProps;
}
export const useConfirmDialog = (props?: TDialogProps) => {
const [state, setState] = useState<IState>({
isDialogOpen: false,
dialogProps: {
...props,
},
});
const setIsDialogOpen = (isOpen = true) => {
setState((prevState) => ({
...prevState,
isDialogOpen: isOpen,
}));
};
// Change dialog props optionally before showing it
const showConfirmDialog = (dialogProps?: TDialogProps) => {
if (dialogProps) {
const updatedProps = { ...state.dialogProps, ...dialogProps };
setState((prevState) => ({
...prevState,
dialogProps: updatedProps,
}));
}
setIsDialogOpen(true);
};
return {
setIsDialogOpen,
showConfirmDialog,
dialogProps: {
isOpen: state.isDialogOpen,
onClose: () => setIsDialogOpen(false),
...state.dialogProps,
},
};
};
But the problem here is the following:
Arguments are passed by reference so if I pass a function to the button (i.e onDeleteGroup) i will keep the function updated to its latest state to perform the correct deletion if a group id changes inside of it.
But as I'm saving the properties inside a state the reference is lost and now I only have the function with the state which it was declared at the beginning.
I tried to add an useEffect to update the hook state when arguments change but this is causing an infinite re render:
useEffect(() => {
setState((prevState) => ({
...prevState,
dialogProps: props || {},
}));
}, [props]);
I know I can call showConfirmDialog and pass the function to update the state with the latest function state but I'm looking for a way to just call the hook, declare the props and not touch the dialog props if isn't needed.
Any answer is welcome, thank you for reading.
You should really consider not doing this, this is not a good coding pattern, this unnecessarily complicates your hook and can cause hard to debug problems. Also this goes against the "single source of truth" principle. I mean a situation like the following
const Component = ({title}: {title?: string}) => {
const {showConfirmDialog} = useConfirmDialog({
title,
// ...
})
useEffect(() => {
// Here you expect the title to be "title"
if(something) showConfirmDialog()
}, [])
useEffect(() => {
// Here you expect the title to be "Foo bar?"
if(somethingElse) showConfirmDialog({title: 'Foo bar?'})
}, [])
// But if the second dialog is opened, then the first, the title will be
// "Foo bar?" in both cases
}
So please think twice before implementing this, sometimes it's better to write a little more code but it will save you a lot debugging.
As for the answer, I would store the props in a ref and update them on every render somehow like this
/** Assign properties from obj2 to obj1 that are not already equal */
const assignChanged = <T extends Record<string, unknown>>(obj1: T, obj2: Partial<T>, deleteExcess = true): T => {
if(obj1 === obj2) return obj1
const result = {...obj1}
Object.keys(obj2).forEach(key => {
if(obj1[key] !== obj2[key]) {
result[key] = obj2[key]
}
})
if(deleteExcess) {
// Remove properties that are not present on obj2 but present on obj1
Object.keys(obj1).forEach(key => {
if(!obj2.hasOwnProperty(key)) delete result[key]
})
}
return result
}
const useConfirmDialog = (props) => {
const localProps = useRef(props)
localProps.current = assignChanged(localProps.current, props)
const showConfirmDialog = (changedProps?: Partial<TDialogProps>) => {
localProps.current = assignChanged(localProps.current, changedProps, false)
// ...
}
// ...
}
This is in case you have some optional properties in TDialogProps and you want to accept Partial properties in showConfirmDialog. If this is not the case, you could simplify the logic a little by removing this deleteExcess part.
You see that it greatly complicates your code, and adds a performance overhead (although it's insignificant, considering you only have 4-5 fields in your dialog props), so I really recommend against doing this and just letting the caller of useConfirmDialog have its own state that it can change. Or maybe you could remove props from useConfirmDialog in the first place and force the user to always pass them to showConfirmDialog, although in this case this hook becomes kinda useless. Maybe you don't need this hook at all, if it only contains the logic that you have actually shown in the answer? It seems like pretty much the only thing it does is setting isDialogOpen to true/false. Whatever, it's your choice, but I think it's not the best idea

Destructure React component into "building blocks"

I am using IonSlides in my app but due to a bug with them, dynamically adding slides can prove difficult.
Because IonSlides is built upon SwiperJS, it has some methods to add and remove slides. The downside to those is that they take a string with HTML in it. In my case, I need to be able to pass in JSX elements so that I can use event listeners on them. Originally, this was my code:
private bindEvents(el: JSX.Element): void {
if (el.props.children) { //Checking if the element actually has children
const children = this.toArray(el.props.children); //If it has only 1 child, it is an object, so this just converts it to an array
children.forEach((c: any) => {
if (!c.props) return; //Ignore if it has no props
const propNames = this.toArray(Object.keys(c.props)); //Get the key names of the props of the child
const el = $(`.${c.props.className}`); //Find the element in the DOM using the class name of the child
propNames.forEach(p => { //Binds the actuall events to the child.
if (Events[p] !== undefined) {
el.on(Events[p], c.props[p]); //`c.props[p]` is the handler part of the event
}
});
});
}
}
Which was called through:
appendSlide(slides: JSX.Element | JSX.Element[]): void {
if (this.slideRef.current === null) return;
this.slideRef.current.getSwiper().then(sw => {
slides = this.toArray(slides);
slides.forEach(s => {
sw.appendSlide(ReactDOMServer.renderToString(s));
this.bindEvents(s);
});
});
}
This worked perfectly when appendSlide was called with an IonSlide:
x.appendSlide(<IonSlide>
<div onClick={() => console.log("Clicked!")}</div>Click me!</IonSlide>
If you clicked the div, it would print "Clicked!".
However, if you pass in a custom component, it breaks. That is because the custom component does not show the children under props. Take this component:
interface Props {
test: string,
}
const TestSlide: React.FC<Props> = (props) => {
return (
<IonSlide>
<div>
{props.string}
</div>
</IonSlide>
);
}
If you were to print that component's props, you get:
props: {test: "..."}
rather than being able to access the children of the component, like I did in the bindEvents function.
There's two ways that I could do fix this. One is getting the JS object representation of the component, like this (I remember doing this ages ago by accident, but I can't remember how I got it):
{
type: 'IonSlide',
props: {
children: [{
type: 'div',
props: {
children: ["..."],
},
}
},
}
or, a slight compromise, destructuring the custom component into its "building blocks". In terms of TestSlide that would be destructuring it into the IonSlide component.
I been trying out things for a few hours but I haven't done anything successful. I would really appreciate some help on this.
For whatever reason that someone needs this, I found you can do el.type(el.props) where el is a JSX element.
This creates an instance of the element so under children instead of seeing the props, you can see the actual child components of the component.

React useCallback does not get updated on state change

The sample below is a simplified excerpt where a child component emits events based on mouse behaviours. React then should update the DOM according to the emitted events.
function SimpleSample() {
const [addons, setAddons] = React.useState<any>({
google: new GoogleMapsTile('google'),
})
const [tooltip, setTooltip] = React.useState<null | { text: string[]; x; y }>(null)
React.useEffect(() => {
// ...
}, [])
const mapEventHandle = React.useCallback(
(event: MapEvents) => {
console.log('event', event.type, tooltip) // LOG 1
if (event.type === MapEventType.mouseoverPopup_show) {
setTooltip({ text: event.text, x: event.coordinates[0], y: event.coordinates[1] })
} else if (event.type === MapEventType.mouseoverPopup_move) {
if (tooltip) setTooltip({ ...tooltip, x: event.coordinates[0], y: event.coordinates[1] })
} else if (event.type === MapEventType.mouseoverPopup_hide) {
setTooltip(null)
}
},
[tooltip]
)
console.log('render', tooltip) // LOG 2
return <MapComponent addons={addons} onEvent={mapEventHandle} />
}
The following order of events is expected:
mouseoverPopup_show is emitted, then tooltip changed from null to a value, a rerender occurs
mouseoverPopup_move is emitted, then tooltip is updated, triggering a rerender
What actually is happening:
Logpoint LOG 2 logs the updated value of tooltip (correct)
When mapEventHandle is called again, the value of tooltip inside that closure (logpoint LOG 1) is never updated, being always null.
Am I missing somethinig? Using the wrong hook?
Here's a codesandbox for it
https://codesandbox.io/s/blissful-torvalds-wm27f
EDIT: On de codesandbox sample setTooltip is not even triggering a rerender
Thanks for the help folks, the issue seems to be down inside a dependency of <MapComponent/>. It ended up saving a reference to the old callback on construction. Still a caveat to watch for, and which i probably wouldnt face with class components...
//something like this
class MapComponent {
emitter = this.props.onChange //BAD
emitter = (...i) => this.props.onChange(...i) //mmkay
}
I think event.coordinates is undefined so event.coordinates[0] causes an error.
If you do: setTooltip({working:'fine'}); you'll get type errors but it does set the toolTip state and re renders.
Thanks to your answer it helped me debug mine which was a bit different. Mine was not working because the callback reference was kept in a state value of the child component.
const onElementAdd = useCallBack(...)..
<Dropzone onElementAdded={props.onElementAdded} />
export const Dropzone = (props: DropzoneProps): JSX.Element => {
const [{ isOver }, drop] = useDrop(
() => ({
accept: props.acceptedTypes,
drop: (item: BuilderWidget, monitor): void => {
if (monitor.didDrop()) return;
props.onElementAdded(item);
},
}),
// missed props.onElementAdded here
[props.onElementAdded, props.acceptedTypes, props.disabled],
);

Bind component on observable boxed primitive value

What I want?
I want a mobx-react component to be binded on boxed observable primitive value from state. So I expect component to rerender if value changes.
Using lodash,
type BusinessData = { root: { path: { value: string } };
#observable state = buildInitialState();
const buildInitialState = () : BusinessData => {root: {}};
<BindedComponent value = {_.get(state, 'root.path.value')} />
const fetchState = () : BusinessData => { root: { path: { value: 'potato' } } };
What is the problem?
At the moment of first application rendering root.path is undefined. It will be fetched later on some stage of some internal component lifecycle or on user action. Furthermore, it might even not be fetched from server. Such path might not exist in data until user edits some input and this value will be set.
Supposable solution - initialize whole state explicitly:
const buildInitialState = () : BusinessData => { root: { path: { value: undefined } } };
Then BindedComponent can bind on boxed undefined and observe changes. This is bad, because when state is deep nested, I have to write such a boilerplate. And also in my case shape of business data can have a lof of implementations. So I have to initialize explicitly every one of them in all my projects.
Any ideas on how I can solve this without boilerplate?
Try to keep your state structure simple:
#observable state = { root: { path: { value: null } }
You can then create a simple update function:
async setStateValue(value) {
try {
this.state.root.path.value = await value
} catch (e) {
console.log(e)
}
}
Calling this at the rendering stage, will automatically update your component once the promise has been completed:
async updateFromComponent() {
await setStateValue('potato')
}
const {value} = prop.store.state.root.path
render (){
return (
<BindedComponent value = {value} />
)
}

Resources