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>
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;
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>(...)
In the following code I can access a context at the parent and then it's undefined in a child. It works locally with simple FC setups, but fails downstream in a class component.
const HookDialog = () => {
const { data, setData } = useDialog(1); // I work fine
return (
<DialogHook>
<DialogContent>
<h1>Value: {data}</h1>
</DialogContent>
<Footer>
<FooterButton name="positive">Positive</FooterButton>
</Footer>
</DialogHook>
);
}
export const FooterButton: React.FC<FooterButtonProps> = (
{
children,
name,
className,
...props
}) => {
const dialogHook = useDialog(); // I'm undefined!
return(
<Button {...props} className={cssNames} ...>
{children}
</Button>
);
}
export const DialogProvider = props => {
const [dialog, setDialog] = useState<ReactElement<typeof Dialog>>();
const [data, setData] = useState<any>();
return (
<DialogContextHook.Provider value={{ dialog, setDialog, data, setData }} {...props} >
{props.children}
{dialog}
</DialogContextHook.Provider>
)
}
type CloseEvent = (source: string, data:any) => void;
interface useDialog extends DialogContextHook {
close: (source: string) => void;
}
export const useDialog = (
initialValue?: any,
onClose?: CloseEvent) => {
const context = useContext(DialogContextHook);
if (!context) {
throw new Error('useDialog must be used within a DialogProvider');
}
useEffect(() => {
context.setData(initialValue);
},[])
const close = (source: string) => {
context.setDialog(undefined);
onClose?.(source, context.data);
}
return { ...context, close };
}
<DialogProvider>
<VOITable/>
</DialogProvider>
Update
I recreated FooterButton in the downstream project and the same code works, just not when imported.
I am working with checkboxes in react typescript. I am handling onChange event but it's not working properly.
code for parent component -
import { myCheckboxData } from "./constant";
export const ParentComponent = (props: Test) => {
const myCheckboxDataAPI = myCheckboxData!.map((data) => Object.assign({}, data));
const initialData: InterfaceA = {
myType: myCheckboxDataAPI!,
};
const [myInitialState, setMyInitialState] = React.useState(initialData);
const onChange = (type: string, selectedValue: boolean) => {
const index: number = myCheckboxDataAPI!.findIndex((x) => x.label === selectType);
if (index > -1) {
myCheckboxDataAPI![index].checked = !selectedValue;
setMyInitialState({
...myInitialState,
myType: myCheckboxDataAPI,
});
}
};
return <FirstChild onChange={onChange} checkboxData={myInitialState} />;
};
code for FirstChild Component -
import react from "react";
interface Test1 {
checkboxData: Array<ICheckboxProps>;
onChange: (type: string, value: boolean) => void;
}
export const FirstChild = (props: Test1) => {
return (
<Grid container wrap='nowrap'>
{myData.map((item) => {
return (
<div key={`new_${item.label}`}>
<Checkbox
id={`new_${item.label}`}
checked={item.checked}
label={item.label}
onChange={(event: React.ChangeEvent<HTMLInputElement>): void => {
onChange(String(item.label), item.checked);
}}
/>
</div>
);
})}
</Grid>
);
};
myCheckboxData has 2 values in it. I am able to select both values one by one. I am also able to select/deselect a particular value. But if I want to deselect both values then it's not working. How can I make it work perfectly ?
I'm still pretty new into the world of TypeScript and I have a couple of questions. I have a few errors in my code, and I'm not rlly sure how to pass the props and choose the right type. I would appreciate a bit of help on that one.
Do you have maybe a good source where I can find all the necessary React info in one place just for the start?
tl;dr
What will be the { data } type? How to define it?
How to pass the functions as props to the Results.tsx file? How define result, results and openPopup in this function?
Did I miss something else?
App.tsx
import React, { useState } from 'react'
import axios from 'axios'
import Search from './components/Search'
import Results from './components/Results'
import Popup from './components/Popup'
export type selected = {
Title?: string,
Year?:number
}
type values = {
s: string,
results: string[],
selected: selected,
}
interface popup {
id: string
}
const App: React.FC = () => {
const [state, setState] = useState<values>({
s: "",
results: [],
selected: {}
});
const apiurl = "http://www.omdbapi.com/?apikey=dfe6d885";
const search = (e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key === "Enter") {
axios(apiurl + "&s=" + state.s).then(({ data }) => {
let results = data.Search;
setState(prevState => {
return { ...prevState, results: results }
})
});
}
}
const handleInput = (e: React.ChangeEvent<HTMLInputElement>) => {
let s = e.target.value;
setState(prevState => {
return { ...prevState, s: s }
});
}
const openPopup = (id : string) => {
axios(apiurl + "&i=" + id).then(({ data }) => {
let result = data;
console.log(result);
setState(prevState => {
return { ...prevState, selected: result }
});
});
}
const closePopup = () => {
setState(prevState => {
return { ...prevState, selected: {} }
});
}
return (
<div className="App">
<header>
<h1>Movie Database</h1>
</header>
<main>
<Search handleInput={handleInput} search={search} />
<Results results={state.results} openPopup={openPopup} />
{(typeof state.selected.Title != "undefined") ? <Popup selected={state.selected} closePopup={closePopup} /> : false}
</main>
</div>
);
}
export default App
Results.tsx
import React from 'react'
import Result from './Result'
function Results ({ results, openPopup }) {
return (
<section className="results">
{results.map(result => (
<Result key={result.imdbID} result={result} openPopup={openPopup} />
))}
</section>
)
}
export default Results
Prop Types
You can defined the props that you pass to a function component inline
function Results ({ results, openPopup }: { results: MyResult[], openPopup: () => void }) {
But it's more common to define a separate interface for the props
interface Props {
results: MyResult[];
openPopup: () => void;
}
Which you can use to define the props directly
function Results ({ results, openPopup }: Props) {
Or through React.FC
const Results: React.FC<Props> = ({ results, openPopup }) => {
Data Type
To define the type for data you need to create an interface that has all of the properties in the API response that you want to use. Then when you call axios, you use the generic type of the axios function to tell it what the fetched data should look like.
For whatever reason, axios() doesn't seem to take a generic, but axios.get() does.
axios.get<MyApiResponse>(apiurl + "&s=" + state.s).then(({ data }) => {
Now the data variable automatically has the MyApiResponse type.