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.
Related
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
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>
);
};
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 am trying to use hooks and React context/provider to show modal in my app. But when I try to show it I am getting this error:
Here is the piece of code:
My Provider
function ModalProvider({ children }) {
const [ModalContent, setModalContent] = useState(null);
const [modalData, setModalData] = useState(null);
const [open, setOpen] = useState(false);
const showModal = useCallback(
({ content, data }) => {
setModalContent(content);
setModalData(data);
setOpen(true);
},
[setModalContent, setModalData, setOpen],
);
const hideModal = useCallback(
() => {
setModalContent(null);
setModalData(null);
setOpen(false);
},
[setModalContent, setModalData, setOpen],
);
const value = useMemo(
() => ({
ModalContent,
open,
modalData,
showModal,
hideModal,
}),
[ModalContent, modalData, open, showModal, hideModal],
);
return (
<ModalContext.Provider value={value}>
<Modal isOpen={open} setIsOpen={setOpen} titleText="Hello from Modal">
{ModalContent && <ModalContent />}
</Modal>
{children}
</ModalContext.Provider>
);
ModalProvider.propTypes = {
children: PropTypes.node,
};
export default memo(ModalProvider);
Implementation
const Modal = () => {
return (
<div><h1>Test Modal</h1></div>
);
}
const Home = ({
logout,
}) => {
const { showModal, hideModal } = useModalDispatcher();
return (
<div>
<Button onClick={() => showModal({ content: Modal })} size="small">SHOW</Button>
<Button onClick={() => hideModal()} size="small">HIDE</Button>
</div>
);
};
Home.displayName = 'Home';
Home.propTypes = {
logout: PropTypes.func.isRequired,
};
export default Home;
The only way that this is working is when I use the prop "type" something like ModalContent.type I do not really know why is that.
The problem here is that you saving the component as a function when using the setModalContent function.
That is so because state can be initialized and updated with a function that returns the initial state or the updated state, you need to supply a function that in turn returns the function you want to put in state.
So, in order to get this working you could wrap the execution of you setModalContent inside the showModal function with an anonymous function, like this:
const showModal = useCallback(
({ content, data }) => {
setModalContent(() => content);
setModalData(data);
setOpen(true);
},
[setModalContent, setModalData, setOpen],
);
Hope it helps!
Can you try changing this, in your ModalProvider component
<Modal isOpen={open} setIsOpen={setOpen} titleText="Hello from Modal">
{ModalContent && <ModalContent />}
</Modal>
To this
<Modal isOpen={open} setIsOpen={setOpen} titleText="Hello from Modal">
{ModalContent && {ModalContent}}
</Modal>
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>