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;
Related
I'm experimenting with extending components in React. I'm trying to extend Handsontable using forwardRef and useImperativeHandle. First I wrap Handsontable in my own BaseTable component, adding some methods. Then I extend the BaseTable in a CustomersTable component in the same way to add even more methods and behavior. Everything seems to work well until I try to consume the CustomersTable in CustomersTableConsumer where I get some type errors. The component works just fine, it's just Typescript that isn't happy.
BaseTable:
export type BaseTableProps = {
findReplace: (v: string, rv: string) => void;
} & HotTable;
export const BaseTable = forwardRef<BaseTableProps, HotTableProps>(
(props, ref) => {
const hotRef = useRef<HotTable>(null);
const findReplace = (value: string, replaceValue: string) => {
const hot = hotRef?.current?.__hotInstance;
// ...
};
useImperativeHandle(
ref,
() =>
({
...hotRef?.current,
findReplace
} as BaseTableProps)
);
const gridSettings: Handsontable.GridSettings = {
autoColumnSize: true,
colHeaders: true,
...props.settings
};
return (
<div>
<HotTable
{...props}
ref={hotRef}
settings={gridSettings}
/>
</div>
);
}
);
CustomersTable:
export type CustomerTableProps = HotTable & {
customerTableFunc: () => void;
};
export const CustomersTable = forwardRef<CustomerTableProps, BaseTableProps>(
(props, ref) => {
const baseTableRef = useRef<BaseTableProps>(null);
const customerTableFunc = () => {
console.log("customerTableFunc");
};
useImperativeHandle(
ref,
() =>
({
...baseTableRef?.current,
customerTableFunc
} as CustomerTableProps)
);
useEffect(() => {
const y: Handsontable.ColumnSettings[] = [
{
title: "firstName",
type: "text",
wordWrap: false
},
{
title: "lastName",
type: "text",
wordWrap: false
}
];
baseTableRef?.current?.__hotInstance?.updateSettings({
columns: y
});
}, []);
return <BaseTable {...props} ref={baseTableRef} />;
}
);
CustomerTableConsumer:
export const CustomerTableConsumer = () => {
const [gridData, setGridData] = useState<string[][]>([]);
const customersTableRef = useRef<CustomerTableProps>(null);
const init = async () => {
const z = [];
z.push(["James", "Richard"]);
z.push(["Michael", "Irwin"]);
z.push(["Solomon", "Beck"]);
setGridData(z);
customersTableRef?.current?.__hotInstance?.updateData(z);
customersTableRef?.current?.customerTableFunc();
customersTableRef?.current?.findReplace("x", "y"); };
useEffect(() => {
init();
}, []);
// can't access extended props from handsontable on CustomersTable
return <CustomersTable data={gridData} ref={customersTableRef} />;
};
Here is a Codesandbox example.
How do I need to update my typings to satisfy Typescript in this scenario?
You need to specify the type of the ref for forwardRef. This type is used then later in useRef<>().
It's confusing, because HotTable is used in useRef<HotTable>(), but BaseTable can't be used the same way, as it is a functional component and because forwardRef was used in BaseTable. So, basically, for forwardRef we define a new type and then later use that in useRef<>(). Note the distinction between BaseTableRef and BaseTableProps.
Simplified example
export type MyTableRef = {
findReplace: (v: string, rv: string) => void;
};
export type MyTableProps = { width: number; height: number };
export const MyTable = forwardRef<MyTableRef, MyTableProps>(...);
// then use it in useRef
const myTableRef = useRef<MyTableRef>(null);
<MyTable width={10} height={20} ref={myTableRef} />
Final solution
https://codesandbox.io/s/hopeful-shape-h5lvw7?file=/src/BaseTable.tsx
BaseTable:
import HotTable, { HotTableProps } from "#handsontable/react";
import { registerAllModules } from "handsontable/registry";
import { forwardRef, useImperativeHandle, useRef } from "react";
import Handsontable from "handsontable";
export type BaseTableRef = {
findReplace: (v: string, rv: string) => void;
} & HotTable;
export type BaseTableProps = HotTableProps;
export const BaseTable = forwardRef<BaseTableRef, BaseTableProps>(
(props, ref) => {
registerAllModules();
const hotRef = useRef<HotTable>(null);
const findReplace = (value: string, replaceValue: string) => {
const hot = hotRef?.current?.__hotInstance;
// ...
};
useImperativeHandle(
ref,
() =>
({
...hotRef?.current,
findReplace
} as BaseTableRef)
);
const gridSettings: Handsontable.GridSettings = {
autoColumnSize: true,
colHeaders: true,
...props.settings
};
return (
<div>
<HotTable
{...props}
ref={hotRef}
settings={gridSettings}
licenseKey="non-commercial-and-evaluation"
/>
</div>
);
}
);
CustomersTable:
import Handsontable from "handsontable";
import React, {
forwardRef,
useEffect,
useImperativeHandle,
useRef
} from "react";
import { BaseTable, BaseTableRef, BaseTableProps } from "./BaseTable";
export type CustomerTableRef = {
customerTableFunc: () => void;
} & BaseTableRef;
export type CustomerTableProps = BaseTableProps;
export const CustomersTable = forwardRef<CustomerTableRef, CustomerTableProps>(
(props, ref) => {
const baseTableRef = useRef<BaseTableRef>(null);
const customerTableFunc = () => {
console.log("customerTableFunc");
};
useImperativeHandle(
ref,
() =>
({
...baseTableRef?.current,
customerTableFunc
} as CustomerTableRef)
);
useEffect(() => {
const y: Handsontable.ColumnSettings[] = [
{
title: "firstName",
type: "text",
wordWrap: false
},
{
title: "lastName",
type: "text",
wordWrap: false
}
];
baseTableRef?.current?.__hotInstance?.updateSettings({
columns: y
});
}, []);
return <BaseTable {...props} ref={baseTableRef} />;
}
);
CustomerTableConsumer:
import { useEffect, useRef, useState } from "react";
import { CustomersTable, CustomerTableRef } from "./CustomerTable";
export const CustomerTableConsumer = () => {
const [gridData, setGridData] = useState<string[][]>([]);
const customersTableRef = useRef<CustomerTableRef>(null);
// Check console and seee that customerTableFunc from customersTable,
// findReplace from BaseTable and __hotInstance from Handsontable is available
console.log(customersTableRef?.current);
const init = async () => {
const z = [];
z.push(["James", "Richard"]);
z.push(["Michael", "Irwin"]);
z.push(["Solomon", "Beck"]);
setGridData(z);
customersTableRef?.current?.__hotInstance?.updateData(z);
customersTableRef?.current?.customerTableFunc();
};
useEffect(() => {
init();
}, []);
return <CustomersTable data={gridData} ref={customersTableRef} />;
};
In your sandbox example it's almost correct, just fix the props type for CustomersTable. I would recommend though to not use Props suffix for ref types, as it is very confusing.
https://codesandbox.io/s/unruffled-framework-1xmltj?file=/src/CustomerTable.tsx
export const CustomersTable = forwardRef<CustomerTableProps, HotTableProps>(...)
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 })
)
}
Consider the following example:
const useCounter = () => {
const [count, setCount] = useState(0);
return [ count, setCount ];
};
const Shower = () => {
const [ value ] = useCounter();
console.log(value); //stays 0
return value;
}
const Setter = () => {
const [ value, setValue ] = useCounter();
console.log(value); //updates on click
return <button onClick={() => setValue(value+1)}>
Add
</button>
}
const App = () => {
return (
<div className="App">
<Setter />
<Shower />
</div>
);
}
What am I doing wrong? I'd expect that it will use the same state no matter where and how many times it gets used, and if that state updates, it should update every component which uses it I think.
Any suggestions?
That's what react context api try to solve.
const CounterContext = React.createContext({
count: 0,
setCount: () => null
})
const CounterProvider = ({ children }) => {
const [count, setCount] = useState(0);
return (
<CounterContext.Provider value={{
count, setCount
}}>
{children}
</CounterContext.Provider>
)
}
const useCounter = () => {
return React.useContext(CounterContext)
};
useCounter will now provide you the same count and setCount in every component you call it.
To use it:
const Shower = () => {
const { count } = useCounter();
return count;
}
const Setter = () => {
const { count, setCount } = useCounter();
return <button onClick={() => setCount(count+1)}>
Add
</button>
}
const App = () => {
return (
<CounterProvider>
<div className="App">
<Setter />
<Shower />
</div>
</CounterProvider>
);
}
useState returns a pair of value and setter. A piece of data and a way to change it, but everytime you instantiate a new Component a new instace of this pair will be created as well. hooks are a great way to share statetul logic between components, not state itself. Shower get's called and a instance of useCounter is created. Setter gets called and a new instance is created. The structure is the same, the state is not.
To share state between components use props, redux or Context API
When sharing things between functional components, I like to use the pattern below, it is the redux-ish reusable version of Federkun's answer above:
// this component should be an ancestor of component sharing state
// note that it works no matter how deep your subcomponents are in the render tree
class SharedStateContextProvider extends React.Component {
/* static propTypes = {
sharedContext: PropTypes.object,
reducer: PropTypes.func,
initialState: PropTypes.object,
children: PropTypes.oneOfType([
PropTypes.node,
PropTypes.arrayOf(PropTypes.node),
]),
} */
constructor(props) {
super(props);
this.state = {
contextValue: { state: props.initialState, dispatch: this.handleDispatch },
};
}
handleDispatch = (action) => {
const { reducer } = this.props;
const { contextValue: { state: sharedState } } = this.state;
const newState = reducer(sharedState, action);
if (newState !== sharedState) {
this.setState(
() => ({
contextValue: { state: newState, dispatch: this.handleDispatch }
})
);
}
}
render() {
const { sharedContext: Context, children } = this.props;
const { contextValue } = this.state;
return (
<Context.Provider value={contextValue}>
{children}
</Context.Provider>
);
}
}
// the actual shared context
const CounterContext = React.createContext();
// add as much logic as you want here to change the state
// as you would do with redux
function counterReducer(state, action) {
switch(action.type) {
case 'setValue':
return {
...state,
value: action.data
};
default:
return state;
}
}
// counterContext is a prop so the dependency in injected
const Shower = ({ counterContext }) => {
// well known redux-ish interface
const { state, dispatch } = React.useContext(counterContext);
console.log(state.value);
return state.value;
}
// counterContext is a prop so the dependency in injected
const Setter = ({ counterContext }) => {
// well known redux-ish interface
const { state, dispatch } = React.useContext(counterContext);
console.log(state.value); //updates on click
return <button onClick={() => dispatch({ type: 'setValue', data: state.value+1 })}>
Add
</button>
}
// the actual shared state
const initialCounterState = { value: 0 };
const App = () => {
return (
<div className="App">
<SharedStateContextProvider
sharedContext={CounterContext}
reducer={counterReducer}
initialState={initialCounterState}
>
<Setter counterContext={CounterContext} />
<Shower counterContext={CounterContext} />
</SharedStateContextProvider>
</div>
);
}
const root = document.getElementById('root');
ReactDOM.render(<App />, root);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.3/umd/react-dom.production.min.js"></script>
<div id="root"></div>
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;