Can't make this message hook work in React

Viewed 85

I'm trying to provide a useMessage hook for my components.

In my application, Panel is the largest component, the mother component, and it has a Message component in it that uses a Material UI's Snackbar to show messages and alerts.

Here's a code sandbox

Components use my hook this way:

import useMessage from './useMessage'

const SomeComponent = () => {
     const { success, error } = useMessage()

     return <button onClick={success('some message')}>Show success</button>
}

However, it does not change the severity and many times the message is empty. It does not work the way I expect it to work.

How can I fix it?

2 Answers

Issue

It seems the useEffect hook in the useMessage hook is the cause of all the trouble, specifically its interaction with the hide function.

const hide = () => {
  setIsMessageShown(false);
};

...

useEffect(() => {
  if (message && severity) {
    setIsMessageShown(true);
  }
  return () => {
    if (isMessageShown === false) {
      setMessage(null);
      setSeverity(null);
    }
  };
}, [
  message,
  setMessage,
  severity,
  setSeverity,
  isMessageShown,
  setIsMessageShown
]);

When hide is called it enqueues a state update to toggle the isMessageShown state false. isMessageShown is a dependency for the useEffect and there's a condition that checks if there's a truthy message and severity state. There is, since that wasn't cleared out too, so another state update is enqueued to toggle the isMessageShown state back true. At the same time, the useEffect hook is returning a cleanup function then enqueues state updates to clear the message and severity state, but the Snackbar and Alert components are still displayed.

There appears to be a state synchronicity issue from here on out and this is why the alert doesn't work the same after the first invocation.

Solution

Commenting out, or removing, the useEffect hook appears to entirely resolve the issue you describe and reproduce in the codesandbox.

Here's an updated useMessage hook.

import { usePanel } from "./PanelContext";

const useMessage = () => {
  const { setIsMessageShown, setMessage, setSeverity } = usePanel();

  const show = (data, action, type) => {
    if (data && data.message) {
      data = data.message;
    }
    setMessage(data);
    setSeverity(type);
    setIsMessageShown(true);
  };

  const success = (data, action) => {
    show(data, action, "success");
  };

  const info = (data, action) => {
    show(data, action, "info");
  };

  const warning = (data, action) => {
    show(data, action, "warning");
  };

  const error = (data, action) => {
    show(data, action, "error");
  };

  const hide = () => {
    setIsMessageShown(false);
  };

  return {
    success,
    info,
    warning,
    error,
    show,
    hide
  };
};

export default useMessage;

I did a bit of refactoring to centralize the Panel context code & logic

PanelContext.js

import React, { useContext, useState } from "react";

export const PanelContext = React.createContext();

export const usePanel = () => useContext(PanelContext);

const PanelProvider = ({ children }) => {
  const [isMessageShown, setIsMessageShown] = useState();
  const [message, setMessage] = useState();
  const [severity, setSeverity] = useState();

  return (
    <PanelContext.Provider
      value={{
        isMessageShown,
        setIsMessageShown,
        message,
        setMessage,
        severity,
        setSeverity
      }}
    >
      {children}
    </PanelContext.Provider>
  );
}

export default PanelProvider;

Edit cant-make-this-message-hook-work-in-react

<button onClick={() => showSuccess()}>Success</button>

I think you should using an arrow function in render

Related