Here's many similar questions but I still couldn't solve this problem.
Child's ref is null in Listner.
I really don't understand what this is.
The code is below.
react 17.0.1
// Parent.tsx
const Parent: React.FC<{id: string}> = (props) => {
const [id] = useState(props.id)
const modalRef = createRef<ModalRef>();
// If I registerd the Listner here, modalRef is not null but,
// multiple Listner has registered.
useEffect(() => {
listner.on('MODAL_POPUP', (o:{param:string}) => {
modalRef.current?.pop(o.param); // <--- modalRef.current is null
});
return() => {};
}, []);
return (
<Modal ref={modalRef} id={id}>
<div>contents</div>
</Modal>
);
};
// Modal.tsx
export interface ModalProps {
id: string;
}
export interface ModalRef {
pop: () => void;
}
const Modal = React.forwardRef<ModalRef, ModalProps>((props, ref) => {
const [id] = useState(props.id);
useImperativeHandle(ref, () => ({
pop() {
console.log('popup modal');
},
}));
return createPotal(
<div>contents..</div>,
document.getElementById('modal-root') as HTMLElement,
);
});
Any advice for me?
Thanks.
You need to use useRef for creating the ref in React Function Components, so change it to this:
const Parent: React.FC<{id: string}> = (props) => {
const [id] = useState(props.id)
const modalRef = useRef<ModalRef>(); // <== here
useEffect(() => {
listner.on('MODAL_POPUP', (o:{param:string}) => {
modalRef.current?.pop(o.param);
});
return() => {
listner.off('MODAL_POPUP', ()=>{});
};
}, []);
return (
<Modal ref={modalRef} id={id}>
<div>contents</div>
</Modal>
);
};
Related
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;
My code
interface ButtonProps {
onClick?: () => void
}
const Button: FC<ButtonProps> = ({ onClick }) => {
const wrapClick = () => {
onClick() // TS2722: Cannot invoke an object which is possibly 'undefined'.
}
return <button onClick={wrapClick}>button</button>
}
Button.defaultProps = {
onClick: () => {},
}
In this case, everything works
const Button: FC<ButtonProps> = ({ onClick }) => {
const wrapClick = () => {
onClick && onClick()
}
return <button onClick={wrapClick}>button</button>
}
and in this
const Button: FC<ButtonProps> = ({ onClick = () => {} }) => {
const wrapClick = () => {
onClick()
}
return <button onClick={wrapClick}>button</button>
}
Is there a way to use default props to solve this problem ? Since I have large components with a lot of properties.
The onClick property in the props object is marked as optional by using a question mark, so we can't directly invoke the function.
To solve the error, use the optional chaining (?.) operator when calling the function.
import { FC } from "react";
interface ButtonProps {
onClick?: () => void;
}
const Button: FC<ButtonProps> = ({ onClick }) => {
const wrapClick = () => {
onClick?.();
};
return <button onClick={wrapClick}>button</button>;
};
Button.defaultProps = {
onClick: () => {}
};
export default Button;
Code Sandbox : DEMO
Use the Non-null assertion operator (postfix !) to let TypeScript know your default onClick(): void is not null/undefined (which it thinks it is because of IButtonProps.onClick: () => void | undefined, which is used by Button (FC<IButtonProps>, FC<T>.defaultProps is of type T & { children?: Node } if I remember correctly)).
interface IButtonProps {
onClick?: () => void; // No event args?
}
const Button: FC<IButtonProps> = (props) => {
const {
onClick = Button.defaultProps.onClick!,
} = props;
const wrapClick = (/* No event args? */) => {
onClick();
};
return <button onClick={wrapClick}>button</button>;
};
Button.defaultProps = {
onClick: () => {},
};
Our project has a dynamic Tab bar, uses redux and custom hooks to manage to add and remove and selection changed. We provide the custom hooks for all routers and actions to add a new tab and display the components relate to it. This tab bar works well with lazy loading in development but always gets 'TypeError: can't resolve read-only property _status of #Object' in production (node sripts/build.js or react-scripts build) even only using React.lazy(() => import). Below are the codes and component stack:
TabHooks:
type AddType = (tabName: string, keepComponent: JSX.Element) => void;
export const useNewAliveTab = (): AddType => {
const dispatch = useDispatch();
const aliveRef = useRef<KeepAlive>();
return (tabName: string, keepComponent: JSX.Element) => {
const now = Date.now().toString();
const keepAliveElement = (
<Suspense fallback={<Loader type="converging-spinner" size="large" />}>
<KeepAlive aliveRef={aliveRef} name={now} key={now}>
<ErrorBoundary>{ keepComponent }</ErrorBoundary>
</KeepAlive>
</Suspense>
);
dispatch(
addNewTab({
tabName: tabName,
uuid: now,
element: keepAliveElement,
})
);
};
};
type DropType = (tabId: string) => void;
export const useDropAliveTab = (): DropType => {
const dispatch = useDispatch();
const { dropScope } = useAliveController();
return (tabId: string) => {
dispatch(removeTab(tabId));
dropScope(tabId);
};
};
type DropCurrentType = () => void;
export const useDropCurrentTab = (): DropCurrentType => {
const dispatch = useDispatch();
const { dropScope } = useAliveController();
const { current } = useSelector((state: RootState) => state.aliveTabs);
return () => {
dispatch(removeTab(current));
dropScope(current);
};
};
TabComponent:
const AliveTabBarComponent = (): JSX.Element => {
const { tabAmount, tabs, current } = useSelector(
(state: RootState) => state.aliveTabs
);
const dispatch = useDispatch();
const dropTab = useDropAliveTab();
const onTabChange = (event: TabStripSelectEventArguments, newValue: string) =>
dispatch(changeSelectedTab(newValue));
return (
<>
<TabStrip selected={tabs.findIndex(item => item.id === current)} onSelect={e => onTabChange(e, tabs[e.selected].id)}>
{tabs.map((tab) => (
<TabStripTab
key={tab.id}
title={
tabAmount !== 0 && (
<GridLayout
gap={{ rows: 6, cols: 6 }}
rows={[{ height: "100%" }]}
cols={[{ width: "90%" }, { width: "10%" }]}>
<GridLayoutItem col={1} row={1}>
<Tooltip anchorElement="target" position="top">
<Typography.p textAlign="center">
{tab.tabName}
</Typography.p>
</Tooltip>
</GridLayoutItem>
<GridLayoutItem col={2} row={1}>
<Tooltip anchorElement="target" position="top">
<Button
iconClass="k-icon k-i-close"
onClick={(e) => {
e.stopPropagation();
dropTab(tab.id);
}}></Button>
</Tooltip>
</GridLayoutItem>
</GridLayout>
)
}>
{tab.keepElement}
</TabStripTab>
))}
</TabStrip>
</>
);
};
export default AliveTabBarComponent;
TabReduxInitState:
interface AliveTabs {
tabs: AliveTabContentList;
current: string;
tabAmount: number;
}
interface AliveTabContent {
tabName: string;
id: string;
keepElement: JSX.Element;
}
type AliveTabContentList = Array<AliveTabContent>;
export const initialAliveTabsState: AliveTabs = {
tabs: new Array<AliveTabContent>(),
current: "",
tabAmount: 0,
};
TabReduxReducers
interface PayloadProps {
uuid: string;
tabName: string;
element: JSX.Element;
}
export const aliveTabsSlice = createSlice({
name: "aliveTabsSlice",
initialState: initialAliveTabsState,
reducers: {
changeSelectedTab(state, action: PayloadAction<string>) {
state.current = action.payload;
},
addNewTab(state, action: PayloadAction<PayloadProps>) {
state.tabAmount++;
state.current = action.payload.uuid;
state.tabs.push({
tabName: action.payload.tabName,
id: action.payload.uuid,
keepElement: action.payload.element,
});
},
removeTab(state, action: PayloadAction<string>) {
const index = state.tabs.findIndex(
(item) => item.id === action.payload
);
const isCurrentTab = state.current === action.payload;
if (index !== -1) {
state.tabAmount--;
state.tabs.splice(index, 1);
if (index === 0) {
if (state.tabAmount > 0) {
if (isCurrentTab) {
state.current = state.tabs[index].id;
}
} else {
state.current = "0";
}
} else if (index > state.tabAmount) {
if (isCurrentTab) {
state.current = state.tabs[state.tabAmount].id;
}
} else {
if (isCurrentTab) {
state.current = state.tabs[index - 1].id;
}
}
}
},
},
});
export default aliveTabsSlice.reducer;
And we use above like this:
const Layout = (): JSX.Element => {
const newTab = useNewAliveTab();
const LazyComponent = React.lazy(() => import("./TestComponent"));
return (
<>
<Button onClick={e => newTab("Test Tab", <LazyComponent />)}>Click Me</Button>
<AliveTabBarComponent />
</>
)
}
We run the codes above very well in development but always get the TypeError in production and the component stack is below:
"
at Lazy
at i (http://localhost:3000/static/js/main.cb249e87.js:2:241027)
at t (http://localhost:3000/static/js/main.cb249e87.js:2:46905)
at t (http://localhost:3000/static/js/main.cb249e87.js:2:46905)
at Suspense
at ke (http://localhost:3000/static/js/main.cb249e87.js:2:50149)
at t (http://localhost:3000/static/js/main.cb249e87.js:2:50398)
at Oe (http://localhost:3000/static/js/main.cb249e87.js:2:51153)
at div
at div
at t (http://localhost:3000/static/js/main.cb249e87.js:2:45629)
at Suspense
at je (http://localhost:3000/static/js/main.cb249e87.js:2:53198)
at t (http://localhost:3000/static/js/main.cb249e87.js:2:53483)
at div
at t (http://localhost:3000/static/js/main.cb249e87.js:2:44132)
at J (http://localhost:3000/static/js/main.cb249e87.js:2:44736)
at t (http://localhost:3000/static/js/main.cb249e87.js:2:56400)"
No idea how to solve. We use this tab to keep alive the components using react-activation, I have tried this is not its problem. And also not the UI framework problem for we have the same issues on Material-UI V4 and Kendo-react.
I had the same error with a similar situation.
I was passing a jsx object with a lazy load element in it into a variable that got used in a dialog component separate from the component I was setting the variable in. A pattern a little like this:
setDialogBody(<><Suspense><LazyLoadedComponent/></Suspense></>);
It was also throwing an error around the 'read-only property _status of #Object' in production.
My solution was to make a new component that contained the LazyLoadComonent and the LazyLoadComponent import logic - I called it a wrapper.
import React, {Suspense} from "react";
const LazyLoadComponent = React.lazy(() => import('./LazyLoadComponent'));
export default function LazyLoadComponentWrapper () {
return <Suspense><LazyLoadComponent/></Suspense>
}
Then I passed that wrapper component into the same pattern:
setDialogBody(<LazyLoadedComponent/>);
I think this simplified it for the minimize code - or shifted the lazyload logic in the same place as where the lazy load was actually taking place in a way that resolved some complication. Anyway, it worked for me.
Perhaps it will work for you too if you try the same approach and use a wrapper component here:
<Button onClick={e => newTab("Test Tab", <LazyComponentWrapper />)}>Click Me</Button>
I want wrapping module for multi use.
so, I make an ItemComponent
export const DragItem = (props: DragProps) => {
const [{... }, fooRef] = useFoo({
})
return (
props.children // how can i send fooRef to here??
)
}
I should send ref to props.children
Is it possible?
check this : https://codesandbox.io/s/gracious-williams-ywv9m
You need to use React.cloneElement to attach/pass extra data to children
export const DragItem = (props: DragProps) => {
const [foo, fooRef] = React.useState({});
var childrenWithRef = React.Children.map(props.children, function(child) {
return React.cloneElement(child, { fooRef: fooRef });
});
return childrenWithRef;
};
I got it.
export const DragItem = (props: DragProps) => {
const [{... }, fooRef] = useFoo({
})
return (
React.cloneElement(props.children, { ref: fooRef })
)
}
I use the library react-use-modal, and
I'm trying to read the updated value of confirmLoading when inside the handleClick function.
handleClick does read the first value of confirmLoading defined when doing const [ confirmLoading, setConfirmLoading ] = useState(false), but never updates when I setConfirmLoading inside handleOk.
I don't understand what I'm doing wrong
import { Button, Modal as ModalAntd } from 'antd'
import { useModal } from 'react-use-modal'
export interface ModalFormProps {
form: React.ReactElement
}
export const ModalForm: React.FC = () => {
const [ confirmLoading, setConfirmLoading ] = useState(false)
const { showModal, closeModal } = useModal()
const handleOk = () => {
setConfirmLoading(true)
setTimeout(() => {
setConfirmLoading(false)
closeModal()
}, 1000)
}
const handleCancel = () => {
closeModal()
}
const handleClick = () => {
showModal(({ show }) => (
<ModalAntd
onCancel={handleCancel}
onOk={handleOk}
title='Title'
visible={show}
>
// the value of confirmLoading is always the one defined
// with useState at the beginning of the file.
<p>{confirmLoading ? 'yes' : 'no'}</p>
</ModalAntd>
))
}
return (
<div>
<Button onClick={handleClick}>
Open Modal
</Button>
</div>
)
}
This is happening because of closures. The component that you pass to showModal remembers confirmLoading and when you call function setConfirmLoading your component renders again and function handleClick is recreated. 'Old' handleClick and 'old' component in showModal know nothing about the new value in confirmLoading.
Try to do this:
export const ModalForm: React.FC = () => {
const { showModal, closeModal } = useModal();
const handleClick = () => {
showModal(({ show }) => {
const [ confirmLoading, setConfirmLoading ] = useState(false);
const handleOk = () => {
setConfirmLoading(true)
setTimeout(() => {
setConfirmLoading(false)
closeModal()
}, 1000)
};
const handleCancel = () => {
closeModal()
};
return (
<ModalAntd
onCancel={handleCancel}
onOk={handleOk}
title='Title'
visible={show}
>
// the value of confirmLoading is always the one defined
// with useState at the beginning of the file.
<p>{confirmLoading ? 'yes' : 'no'}</p>
</ModalAntd>
);
})
};
return (
<div>
<Button onClick={handleClick}>
Open Modal
</Button>
</div>
)
}