user2350374
user2350374

Reputation: 85

React useContext not causing a rerender

So I am attempting to use a custom hook that utilizes useContext which I would expect to cause a rerender of my ToastContainer component when ToastContext changes. However, the ToastContainer component is not rerendering when the context changes. When using dev tools I can see that the context is indeed changed by the hook, but the new data is not being displayed.

Sorry for all the code, I'm just not sure where the bug lies

useToast.js

function useToast () {
  let [toasts, setToasts] = useContext(ToastContext)
  

  function createToast(message, color, duration = 0) {
    let id = Math.floor(Math.random() * 1000)
    toasts.set(id, <Toast {...{ message, color, duration, id }} />)
    setToasts(toasts)

    if (duration) {
      setTimeout(() => { toasts.delete(id); setToasts(toasts)}, duration * 1000)
    }
  }

  return [toasts, createToast]
}

ToastContainer.js

function ToastContainer (props) {
  let [toasts, setToasts] = useContext(ToastContext)
  return( <> {[...toasts.values()]} </>)
}

page.js

function Page (props) {  
    let [toasts, createToast] = useToast()  
    createToast("hello", 'red')
    createToast("world", 'yellow')

    return(<Article />)
}

app.js

function App({Component, pageProps}) {


  const toastState = useState(new Map())

  return (
    <>
          <ToastContext.Provider value={toastState}>
            <ToastContainer/>
            <main>
                <Component {...pageProps}></Component>
            </main>
          </ToastContext.Provider>
    </>
  )

Upvotes: 1

Views: 2719

Answers (1)

Anthony
Anthony

Reputation: 6482

So a couple of things:

By calling toasts.set(id, <Toast {...{ message, color, duration, id }} />) you are directly mutating your state which you don't want to do. You then call setToasts with the exact same Map object, so it won't trigger a re-render as it's the same reference.

Also, if this were working, by calling createToast() in your functional component as it renders, you would have triggered a Maximum update depth exceeded exception as it would have:

  • rendered
  • created a toast, triggering a re-render
  • re-rendered
  • created a toast, triggering a re-render
  • re-rendered ...and so on

You should move the creation of a Toast to be event-driven, on a click of a button or something that makes sense.

You can use Map, but you would need to do something like:

const [myMap, setMyMap] = useState(new Map());
const updateMap = (k,v) => {
  setMyMap(new Map(myMap.set(k,v)));
}

as seen on https://medium.com/swlh/using-es6-map-with-react-state-hooks-800b91eedd5f. This will create a new Map object with the key-value pairs of the current Map.

Alternately you can use an object {}, with a couple of tweaks:

const toastState = useState({});

setToasts({
  ...toasts,
  [id]: <Toast key={id} {...{ message, color, duration, id }} />
});

function ToastContainer (props) {
  let [toasts, setToasts] = useContext(ToastContext)
  return Object.values(toasts);
}

if (duration) {
  setTimeout(() => {
    const newToasts = { ...toasts };
    delete newToasts[id];
  }, duration * 1000)
}

Upvotes: 1

Related Questions