0

Besides prop value updates in a hook, I need to bind to events that get triggered in the hook too. So the consumer of the hook can bind to the event-like addEventListner, removeEventListener. How do I do this?

What I have so far:

import {useState, useEffect} from 'react';

interface MyHookProps {
  name: string;
  onChange: () => void;
}

const useNameHook = () : MyHookProps => {
  const [name, setName] = useState<string>('Anakin');

  const onChange = () => {

  }

  useEffect(() => {
    setTimeout(() => {
      setName('Vader');
      // how to a raise an onChange event here that consumers could bind to?
    }, 1000);
  }, []);

  return {
    name,
    onChange,
  }
}

export default function App() {
  const {name, onChange} = useNameHook();

  const handleHookChange = () => {
    console.info('hook changed', name);
  }

  return (
    <div className="App">
      <h1>Hello {name}</h1>
    </div>
  );
}
Ken White
  • 123,280
  • 14
  • 225
  • 444
Easy E
  • 3
  • 1
  • "I need to bind to events that get triggered in the hook too." Why do you need to do this? What problem are you attempting to solve by doing so? – Code-Apprentice Jun 17 '21 at 23:27
  • The demo above is simplified, but I have the need to be notified of certain events within a hook i.e. media query match changed, a countdown completed, etc etc – Easy E Jun 17 '21 at 23:40

1 Answers1

0

I think you can refer to the 'Declarative' pattern here.

Reading this article about 'Making setInterval Declarative with React Hooks' from Dan Abramov really changed my ways of thinking about the React hooks.

https://overreacted.io/making-setinterval-declarative-with-react-hooks/

So, my attempts to make this useName hook declarative is like below:

// hooks/useName.ts
import { useEffect, useRef, useState } from "react";

type Callback = (name: string) => void;

const useName: (callback: Callback, active: boolean) => string = (
  callback,
  active
) => {
  // "Listener"
  const savedCallbackRef = useRef<Callback>();

  // keep the listener fresh
  useEffect(() => {
    savedCallbackRef.current = callback;
  }, [callback]);

  // name state
  const [internalState, setInternalState] = useState("anakin");

  // change the name after 1 sec
  useEffect(() => {
    const timeoutID = setTimeout(() => {
      setInternalState("vader");
    }, 1000);
    return () => clearTimeout(timeoutID);
  }, []);

  // react to the 'name change event'
  useEffect(() => {
    if (active) {
      savedCallbackRef.current?.(internalState);
    }
  }, [active, internalState]);

  return internalState;
};

export default useName;

and you can use this hook like this:

// App.ts
import useName from "./hooks/useName";

function App() {
  const name = useName(state => {
    console.log(`in name change event, ${state}`);
  }, true);
  return <p>{name}</p>;
}

export default App;

Note that the 'callback' runs even with the initial value ('anakin' in this case), and if you want to avoid it you may refer to this thread in SO: Make React useEffect hook not run on initial render

Hyunwoong Kang
  • 530
  • 2
  • 9
  • 1
    Apologies @Hyunwoong, I will take a look through this shortly and respond. Thanks for the answer to review. – Easy E Jun 19 '21 at 01:57