Reputation: 85
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
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:
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