skip to Main Content

I’m trying to trigger an event from a Parent component to a Child component and I don’t find any suitable solution for my case.

My use case is a Parent component which handle save action by displaying buttons or else, and a Child component which need to know when he has to save his data.

The only way which satisfies me is by using RxJS, but I feel it’s not a good practice.

I specify that I’m in a context where I try to write an UI library, so I try to simplify and type props as much as possible.

Moreover, my Child component is injected in the Parent component dynamically, so I can’t just wrap the Child component with Parent and pass a callback to Parent component.

I thought about two solutions:

  1. By using a boolean

My Parent component can handle a boolean and pass it to the Child component. My Child component can react when the boolean changes.

I think it is more a workaround than a real solution.

  1. By using a ref

The Parent component explicitly calls a function defined in Child component.

Why not, but I think it’s quite ugly. I loose typing, my Child component

Best solution for me using RxJS

If I could use RxJS I would pass an observable from Parent to Child, like this:

const Parent = () => {

  const [subject] = useState(() => {
    return new Subject<void>()
  })

  const onSaveClicked = () => {
    subject.next()
  }

  return <>
    <div>
      <button onClick={onSaveClicked}>
        Save something
      </button>
    </div>
    <div>
      <Child saveObservable={subject.asObservable()}/>
    </div>
  </>
}
const Child = ({saveObservable}: {saveObservable: Observable<void>}) => {

  const [saveCounter, setSaveCounter] = useState(0)

  useEffect(() => {
    const subscription = saveObservable.subscribe({
      next: () => {
        // do someting, save for exemple

        // Here I increment the count to show the event is well handled
        setSaveCounter(x => x + 1)
      }
    })

    return () => subscription.unsubscribe()
  }, [saveObservable]);

  return <>
    Save triggered count: {saveCounter}
  </>
}

I don’t understand why there is no mechanism already existing which allow me to do that, I feel like I messed something.

Someone can enlighten me ?

3

Answers


  1. There is a Context API in react which you can leverage to achieve your requirement. With context, you can pass data down the component tree without passing props manually at every level(prop-drilling). You can create a context to handle the save action and consume it in the child component and it also simplifies the component structure and eliminates the need for third-party libraries like RxJS. Let me help you with your use-case using this.

    1. I would create a custom hook and a Provider.

        // SaveCotext.jsx
       import { createContext, useContext, useState } from 'react';
      
        const SaveContext = createContext();
      
         const useSave = () => useContext(SaveContext);
      
         const SaveProvider = ({ children }) => {
         const [saveTrigger, setSaveTrigger] = useState(false);
      
         const triggerSave = () => {
           setSaveTrigger((prev) => !prev);
         };
      
        return (
          <SaveContext.Provider value={{ saveTrigger, triggerSave }}>
             {children}
          </SaveContext.Provider>
         );
       };
      
       export { SaveProvider, useSave };
      
    2. Then would wrap my Parent Component with the SaveProvider.

      // ... rest of the code
       <SaveProvider>
         <Parent />
       </SaveProvider>
      );
      
    3. Finally, I would use the save context in Parent and Child components:

      const Parent = () => {
      const { triggerSave } = useSave();
      
      return (
         <div>
          <button onClick={triggerSave}>Save something</button>
          <Child />
        </div>
       );
      };
      
      const Child = () => {
      const { saveTrigger } = useSave();
      const [saveCounter, setSaveCounter] = useState(0);
      
      useEffect(() => {
        // Do something, save for example
        setSaveCounter((prev) => prev + 1);
      }, [saveTrigger]);
      
       return <>Save triggered count: {saveCounter}</>;
      };
      

    SAMPLE DEMO


    Note:

    1. If you notice in the sample demo I shared initially Save Triggered Count is 1 although the state is initialized with 0. The reason for it is setSaveCounter() is called inside of useEffect and on initial render it will trigger the setSaveCounter().
    2. In the sample demo I have removed the <StrictMode>. If you’re using it(which you should and is strongly recommended in development mode in React 18.x.x) you will notice the saveCounter initializes from 2. The reason for this can be found here.
    Login or Signup to reply.
  2. You can preserve typing with useRef, and I think it’s probably the way to go with your constraints. You could also use the Context API, but that might reduce the reusability of your component, since you will create a dependency on the context. If the dependency issue is fine for you, then that might be a viable and clean option. Using ref, however, will allow you plug that component in anywhere without having to depend on a shared Context.

    Here is an example of that working: https://playcode.io/1744194

    interface stuffHandler {
      doStuff: () => void;
    }
    
    interface childProps {
      prop: string;
    }
    
    const Child = React.forwardRef<stuffHandler, childProps>((props, ref) => {
      const refHandler = React.useRef(null)
      const [saveCounter, setSaveCounter] = React.useState(0)
    
     
      const incrementSave = () => {
        setSaveCounter(saveCounter + 1);
      }
      React.useImperativeHandle(ref, () => {
        return {
          doStuff: incrementSave
        }
      })
    
      return (<>
        {props.prop}: {saveCounter}
      </>
    
      )
    })
    
    export function Parent() {
      const childRef = React.useRef<stuffHandler>(null)
    
    
      const onSaveClicked  = () => {
        childRef.current.doStuff();
      }
    
      return (
        <>
    
          <input type="button" onClick={() => onSaveClicked ()} value="Save Something" />
          <Child prop={"TEXT"} ref={childRef} />
        </>
      )
    }
    
    Login or Signup to reply.
  3. Declare subject in Parent:

     const [subject] = createSubject();
    

    Declare createSubject outside Parent:

    const createSubject = () => {
      const [saveTrigger, setSaveTrigger] = useState(0);
    
      const next = () => setSaveTrigger(saveTrigger + 1);
    
      const subscribe = ({ next = () => null }) => {
        if (saveTrigger) {
          // do otherstuff
          next();
        }
        return {
          unsubscribe: () => {
            // doSomthing
          },
        };
      };
    
      const asObservable = () => {
        return {
          subscribe,
          saveTrigger,
        };
      };
    
      return [{
          next,
          asObservable,
        }];
    };
    

    Below is the concept, following React suggestion to avoid useEffect for React updates.

    const ParrentComponent = ({ contentToBeSaved }) => {
      const [saveTrigger, setSaveTrigger] = React.useState(0); // or true false
    
      const onSaveClicked = () => {
        setSaveTrigger(saveTrigger + 1);
      };
    
      return (
        <>
          {/* ...otherComponents */}
          <ChildComponent
            contentToBeSaved={contentToBeSaved}
            saveTrigger={saveTrigger}
          />
        </>
      );
    };
    
    const ChildComponent = ({ contentToBeSaved, saveTrigger }) => {
      const [prevSaveTrigger, setPrevSaveTrigger] = React.useState(saveTrigger);
    
      if (prevSaveTrigger !== saveTrigger) {
        setPrevSaveTrigger(saveTrigger);
        // handle saving
      }
      return <div>Save trigger: {prevSaveTrigger}</div>;
    };
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search