Susitha Ravinda Senarath
Susitha Ravinda Senarath

Reputation: 1678

React custom hook to to log page render start event

I'm writing a logging framework for a react js app for performance monitoring. Basically there are two methods provided by sdk to create a time event and and to stop measuring and push timing details to where ever it wants. Since I need to capture the data fetching part too I thought of storing the created timing event in redux store so later can stop the timer when data is fetched. In order to decouple view components from logging as much as possible I'm trying to write two custom hooks for following purposes.

  1. Create timing event if nothing available in state with given key and store it in state
  2. Look for timing event in store by provided key and stop the timer

Following are the hooks I have created.

export const useStartRenderLogger = (
  component: string,
  key: string,
  eventParams?: any
): void => {
  const dispatch = useDispatch();
  const existingTimer = useGetTimerByKey(key); <= Selector which return timer for key
  const metrics = useMetrics(component); <= sdk hook to create a metric instance
  if (existingTimer) return;
  const timer = metrics.start(key, {
    ...eventParams,
    stage: TimeLogStages.Render,
  });
  dispatch(TimeLogActions.pushLogger(key, timer)); <= dispatch passing timing log and key so it can stored on state
};

export const useStopRenderLogger = (
  key: string,
  pageRendered: boolean,
  params?: any
): void => {
  const timer = useGetTimerByKey(key);
  const dispatch = useDispatch();
  if (!pageRendered || isUndefined(timer)) return;
  timer.stop(params);
  dispatch(TimeLogActions.popLogger(key));
};

I call first one at the beginning of the component and the other right before the render passing boolean to indicate whether data has been fetched.

The problem is useStartRenderLogger throws a error related to react rules of hooks.

index.js:1 Warning: React has detected a change in the order of Hooks called by MyComponent. This will lead to bugs and errors if not fixed. For more information, read the Rules of Hooks

It seems the dispatch is casing the issue. I guess pushing event into state make useGetTimerByKey selector to update and cause the error. If that line is commented then page renders without errors.

Is my assumption about why error get thrown is correct? Can I make useGetTimeByKey selector not to update when state change?

Upvotes: 0

Views: 543

Answers (1)

Linda Paiste
Linda Paiste

Reputation: 42228

Problem: return Before a Hook

The problem is these two lines:

if (existingTimer) return;
const metrics = useMetrics(component);

You must call the same hooks in the same order on every render. Calling a hook cannot be conditional. So you cannot have a return above a hook call in your code, as that means useMetrics is only sometimes called.

If you want to have some conditional logic, you can move that to inside the useMetrics hook.

It's ok to make pure functions conditional. Calling dispatch(action) can be conditional -- but calling useDispatch cannot. So this code is ok:

export const useStartRenderLogger = (
  component: string,
  key: string,
  eventParams?: any
): void => {
  // call all hooks first
  const dispatch = useDispatch();
  const existingTimer = useGetTimerByKey(key);
  const metrics = useMetrics(component);
  // only return after all hooks are called
  if (existingTimer) return;
  // conditionally executed
  const timer = metrics.start(key, {
    ...eventParams,
    stage: TimeLogStages.Render,
  });
  dispatch(TimeLogActions.pushLogger(key, timer));
};

Upvotes: 1

Related Questions