Reputation: 319
I have a global component called <MyGlobalComponent/>
that resides in <App/>
. What I am trying to achieve is that I want to build a global function showGlobalComponent()
that can be called from any other component, one which sets its visibility and the content rendered inside it (which is another component).
MyGlobalComponent.tsx
export type GlobalComponentProps {
Component: React.FC
isVisible: boolean
};
export function showGlobalComponent ({Component, isVisible}: GlobalComponentProps) {
// ??? What do I do here ???
// Supposedly I'd set isVisible & ComponentToRender state variables here, but I can't, I'm outside the function
}
function MyGlobalComponent() {
const [ComponentToRender, setComponentToRender] = useState<React.FC | null>(null);
const [isVisible, setIsVisible] = useState<boolean>(false)
return (
<View style={APPSTYLE.myComponentStyle} />
{
isVisible &&
<>
ComponentToRender
</>
}
</View>
)
Here is the example of how I'd call it
AnotherComponent.tsx
function AnotherComponent() {
useEffect(() => {
showGlobalComponent({
isGlobalComponentVisible: true,
Component => <TertiaryComponent myProp="asds"/>
})
}, [])
}
I thought about using Redux and only memorizing a string associated with the component I want to render inside <MyGlobalComponent/> (because you cannot memoize non-serializable stuff with Redux). However, if I want to pass a prop to it like I did above to
TertiaryComponent`, that becomes quite impossible.
A solution I thought of (which I'm in doubt about) is using DeviceEventEmitter
from react-native
to emit an event and then listening to it inside an useEffect
in <MyGlobalComponent/>
MyGlobalComponent.tsx
export function showGlobalComponent (
{ Component, isVisible }: GlobalComponentProps
) {
DeviceEventEmitter.emit("myEventName", Component, isVisible);
}
// ...
useEffect(() => {
const myEvent = DeviceEventEmitter.addListener(
"myEventName",
({ Component, isVisible }: GlobalComponentProps) => {
setIsVisible(isVisible)
setComponentToRender(Component)
}
);
return () => {
myEvent.remove();
};
}, []);
However, the behaviour of this is inconsistent and this doesn't seem the right way to do it. Besides, DeviceEventEmitter
is very poorly documented.
What's a more common or practical way to achieve this?
Upvotes: 1
Views: 127
Reputation: 203091
Your code is not React-ing very correctly. Instead of declaring some unrestricted global and mutating it locally you should create a React Context provider to hold the state and provide the component and visibility value down to consumers.
Example:
MyGlobalComponentProvider.tsx
import {
PropsWithChildren,
ReactNode,
Dispatch,
createContext,
useContext,
useState
} from "react";
type GlobalComponentProps = {
component: ReactNode;
setComponent: Dispatch<ReactNode>;
isVisible: boolean;
setIsVisible: Dispatch<boolean>;
};
export const MyGlobalComponentContext = createContext<GlobalComponentProps>({
component: null,
setComponent: () => null,
isVisible: false,
setIsVisible: () => false
});
export const useMyGlobalComponentContext = () =>
useContext(MyGlobalComponentContext);
export const MyGlobalComponent = () => {
const { component, isVisible } = useMyGlobalComponentContext();
return <div>{isVisible ? component : null}</div>;
};
const MyGlobalComponentProvider = ({ children }: PropsWithChildren<{}>) => {
const [component, setComponent] = useState<ReactNode>(null);
const [isVisible, setIsVisible] = useState(false);
return (
<MyGlobalComponentContext.Provider
value={{
component,
isVisible,
setComponent,
setIsVisible
}}
>
{children}
</MyGlobalComponentContext.Provider>
);
};
export default MyGlobalComponentProvider;
Import the MyGlobalComponentProvider
component and wrap the app or root-level component to provide the context value to that sub-ReactTree. Consumers use the exported useMyGlobalComponentContext
hook to access the values and handle/render accordingly.
App
import MyGlobalComponentProvider, {
MyGlobalComponent,
} from "./MyGlobalComponentProvider";
export default function App() {
return (
<MyGlobalComponentProvider>
<div className="App">
<MyGlobalComponent />
<AnotherComponent />
</div>
</MyGlobalComponentProvider>
);
}
AnotherComponent.tsx
import { useEffect } from 'react';
import {
useMyGlobalComponentContext
} from "./MyGlobalComponentProvider";
const AnotherComponent = () => {
const { setComponent, setIsVisible } = useMyGlobalComponentContext();
useEffect(() => {
setComponent(<TertiaryComponent myProp="asds" />);
setIsVisible(true);
}, []);
return <h1>AnotherComponent</h1>;
};
Upvotes: 2
Reputation: 487
you can use references and reference manipulation hooks such as useImperativeHandle for this job.
I prepared an example because I thought it might guide you. An additional way to communicate between components is to use global references.
These methods are generally used in my own projects, such as Modal, Toast, etc., which will be used throughout the application. I use it for elements. Here is the example;
import React, {useImperativeHandle, useState} from 'react';
import {Button, View} from 'react-native';
const GlobalComponentRef = React.createRef<GlobalComponentRef>();
const App = () => {
return (
<View>
<GlobalComponent ref={GlobalComponentRef} />
<AnotherComponent />
</View>
);
};
type GlobalComponentProps = {Component?: React.FC; visible?: boolean};
type GlobalComponentRef = {
state: () => GlobalComponentProps;
setComponent: (component: React.FC) => void;
setVisible: (bool: boolean) => void;
};
const GlobalComponent = React.forwardRef<
GlobalComponentRef,
GlobalComponentProps
>(
(
{Component = React.Fragment, visible = false}: GlobalComponentProps,
ref,
) => {
const [_Component, setComponent] = useState<React.FC>(Component);
const [_visible, setVisible] = useState<boolean>(visible);
useImperativeHandle(
ref,
() => {
return {
state: () => ({visible: _visible, Component: _Component}),
setComponent: (component: React.FC) => setComponent(component),
setVisible: (bool: boolean) => setVisible(bool),
};
},
[_Component, _visible],
);
return <View>{visible && <>ComponentToRender</>}</View>;
},
);
const AnotherComponent = () => {
const onPress = () => {
const {visible} = GlobalComponentRef.current?.state() || {};
GlobalComponentRef.current?.setVisible(!visible);
};
return (
<View>
<Button title={'Click Me'} onPress={onPress} />
</View>
);
};
Upvotes: 1
Reputation: 1156
You need to React.forwardRef to your global component first. Then you can assign show/hide functions using React.useImperativeHandle hook.
https://github.com/calintamas/react-native-toast-message/blob/main/src/Toast.tsx
Check this implementation, it might help you solve the issue.
Upvotes: 1