Setting a component's state using a function that can be called from everywhere

83 views Asked by At

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?

3

There are 3 answers

4
Drew Reese On BEST ANSWER

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>;
};

enter image description here

0
ko100v.d On

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.

0
HyopeR On

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>
  );
};