In nextjs . I want to block navigation and appear a confirmation window popup when trying to navigate to another page - javascript

In nextjs . I want to block navigation and appear a confirmation window popup when user trying to navigate to another page. the navigation should continue if the user click yes in the confirmation popup. If the user clicks the "no" in confirmation window , the user should stay in the current page.
There is no way in the official nextjs documentation about blocking a router.

You can use JavaScript's window.onbeforeunload event to do that.
See an example code below
const ExamplePage = () => {
useEffect(() => {
window.onbeforeunload = () => true;
return () => {
window.onbeforeunload = null;
};
}, []);
return (
<div>
<h1>Example Page</h1>
<p>This page will display a confirmation dialog when navigating away.</p>
</div>
);
};
export default ExamplePage;

If you are using NextJS 12 or less you can have the following:
Stack
NextJS 12.x
Typescript
Tailwind
Some custom components, replace on your own.
The idea behind the components below is to get when the router fires the routeChangeStart and stop it immediately, then we have two ways of getting user confirmation, you can use the window.confirm() which cannot be styled, so you probably don't want that or you can trigger an async function (which is used on the example), pass a callback so you can continue the action in case the user press "Yes" and throw immediately after, if you don't do that, it will continue the route change.
BeforePopState is when you fire the go back / go forward on the router/browser.
BeforeUnload is when the user refreshes or clicks to exit the page (in this case you can only show the browser default window.
To help you understand better and implement, here is the logic I have created.
Code examples
/hooks/useWarnIfUnsaved
import getUserConfirmation, {
IConfirmationBody,
IConfirmationDialogConfig,
} from "#/utils/ui/confirmationDialog"
import Router from "next/router"
import { useEffect, useRef } from "react"
import useWarnBeforeUnload from "./useWarnBeforeUnload"
export interface TConfirmationDialog {
body: IConfirmationBody
config?: IConfirmationDialogConfig
}
/**
* Asks for confirmation to leave/reload if there are unsaved changes.
* #param {boolean} unsavedChanges Whether there are unsaved changes. Use a ref to store the value or make a comparison with current and previous values.
* #param {TDialog} confirmationDialog [Optional] The dialog to show.
* #returns {void}
*/
export default function useWarnIfUnsaved(
unsavedChanges: boolean,
confirmationDialog: TConfirmationDialog
) {
const unsavedRef = useRef(unsavedChanges)
// * Keep unsaved in sync with the value passed in.
// * At the same time we want to be able to reset
// * the trigger locally so we can move forward on the redirection
useEffect(() => {
unsavedRef.current = unsavedChanges
}, [unsavedChanges])
useWarnBeforeUnload(unsavedRef.current)
useEffect(() => {
const handleRouteChange = (url: string) => {
if (unsavedRef.current && Router.pathname !== url) {
Router.events.emit("routeChangeError")
getUserConfirmation(confirmationDialog.body, {
...confirmationDialog.config,
callbackAfterConfirmation: () => {
unsavedRef.current = false
Router.replace(url)
},
})
throw "Route change aborted. Ignore this error."
}
}
Router.beforePopState(({ url }) => {
if (unsavedRef.current) {
if (Router.pathname !== url) {
getUserConfirmation(confirmationDialog.body, {
...confirmationDialog.config,
callbackAfterConfirmation: () => {
unsavedRef.current = false
window.history.pushState("", "", url)
},
})
return false
}
}
return true
})
// For changing in-app route.
if (unsavedRef.current) {
Router.events.on("routeChangeStart", handleRouteChange)
} else {
Router.events.off("routeChangeStart", handleRouteChange)
}
return () => {
Router.beforePopState(() => true)
Router.events.off("routeChangeStart", handleRouteChange)
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [unsavedRef.current])
}
#/utils/ui/confirmationDialog
import Button from "#/components/Buttons/Button.primitive"
import DynamicIcon from "#/components/Icons/DynamicIcon"
import Text from "#/components/Typography/Text.primitive"
import { PolarisIcons } from "#/types/polarisIcons.types"
import { createRoot } from "react-dom/client"
import { cn } from "../className"
export interface IConfirmationDialogConfig {
overrideBody?: JSX.Element
cancelButtonText?: string
okButtonText?: string
title?: string
hideOkButton?: boolean
confirmationInput?: string
className?: string
bodyClassName?: string
isDeleteConfirmation?: boolean
callbackAfterConfirmation?: () => void
callbackAfterCancel?: () => void
}
export interface IConfirmationBody {
icon?: PolarisIcons | JSX.Element
title: string
message: string
}
interface IRenderDialogProps {
resolve: (value: { value: string } | PromiseLike<{ value: string }>) => void
body: IConfirmationBody
config?: IConfirmationDialogConfig
}
type TPromiseResolve = (
value:
| {
value: string
}
| PromiseLike<{
value: string
}>
) => void
const renderDialog = (
body: IConfirmationBody,
resolve: TPromiseResolve,
config?: IConfirmationDialogConfig
) => {
const root = document.querySelector("body")
if (!root) {
console.error("No root element found.")
return
}
const div = document.createElement("div")
div.setAttribute("id", "confirmationDialogContainer")
div.setAttribute(
"class",
"h-screen w-screen fixed z-50 inset-0 grid place-items-center globalConfirmationModalContainer"
)
root.appendChild(div)
const container = document.getElementById("confirmationDialogContainer")
if (container === null) {
console.error("Container was not found.")
}
const dialog = createRoot(container as HTMLElement)
return dialog.render(
<ConfirmationDialog body={body} resolve={resolve} config={config} />
)
}
const removeDialog = () => {
const root = document.querySelector("body")
if (!root) {
console.error("No root element found.")
return
}
const divs = root.querySelectorAll(".globalConfirmationModalContainer")
divs && divs.forEach((div) => root.removeChild(div))
}
const ConfirmationDialog = ({ resolve, body, config }: IRenderDialogProps) => {
const clickOK = () => {
removeDialog()
config?.callbackAfterConfirmation?.()
resolve({ value: "true" })
}
const clickCancel = () => {
removeDialog()
config?.callbackAfterCancel?.()
resolve({ value: "" })
}
return (
<div
className="fixed inset-0 grid h-screen w-screen place-items-center"
style={{ zIndex: 9999 }}
>
<div
className="fixed inset-0 z-0 h-screen w-screen bg-black opacity-60"
onClick={clickCancel}
onKeyDown={() => undefined}
role="button"
tabIndex={0}
/>
<div className="dark:bg-blue-dark relative z-10 max-w-[95vw] rounded-lg bg-white shadow-lg">
<div className="p-14">
{config?.overrideBody ? (
<></>
) : (
<>
<div className="flex items-center justify-between border-b border-ds-gray-300 p-6">
<Text
as="p"
className="text-sm font-extrabold"
color="text-ds-primary-700 dark:text-ds-primary-200"
>
{config?.title || "Confirmation"}
</Text>
<Button
variant="icon"
subvariant="solo"
onClick={clickCancel}
iconName={PolarisIcons.CancelMajor}
className="border-none !p-0"
/>
</div>
<div
className={cn(
"flex flex-col items-center justify-center px-7 py-12",
config?.bodyClassName
)}
>
{body.icon && (
<div className="mb-4 flex items-center justify-center rounded-full">
{typeof body.icon === "string" ? (
<DynamicIcon
iconName={body.icon as PolarisIcons}
width={120}
className="max-w-[25%] bg-ds-gray-400 dark:bg-ds-gray-700"
/>
) : (
body.icon
)}
</div>
)}
<div className="px-8 text-center">
<Text
as="h3"
className="mb-2 text-lg font-extrabold leading-[1.44]"
color="text-ds-primary-700 dark:text-ds-primary-200"
>
{body.title}
</Text>
<Text
as="p"
className="text-sm leading-[1.57] text-ds-gray-600 dark:text-ds-gray-400"
font="inter"
>
{body.message}
</Text>
</div>
</div>
<div className="flex items-center justify-end gap-3 border-t border-ds-gray-300 p-6">
<Button
variant="main"
subvariant="outlinePrimary"
size="md"
onClick={clickCancel}
className="text-ds-gray-600 dark:text-ds-gray-400"
>
{config?.cancelButtonText || "Cancel"}
</Button>
{!config?.hideOkButton && (
<Button
variant="main"
subvariant={
config?.isDeleteConfirmation ? "error" : "primary"
}
onClick={clickOK}
size="md"
>
{config?.okButtonText || "Ok"}
</Button>
)}
</div>
{/* <Text
as="h1"
label="Modal Title"
color="text-mirage dark:text-link-water"
>
{config?.title || "Confirmation"}
</Text>
<Text as="p">{body}</Text>
</div>
<div className="mt-10 flex items-center justify-center gap-3">
<Button
variant="main"
subvariant={
config?.isDeleteConfirmation ? "outlinePrimary" : "primary"
}
onClick={clickCancel}
>
{config?.cancelButtonText || "Cancel"}
</Button>
{!config?.hideOkButton && (
<Button
variant={config?.isDeleteConfirmation ? "5" : "2"}
onClick={clickOK}
className="px-8"
>
{config?.okButtonText || "Ok"}
</Button>
)}
</div>*/}
</>
)}
</div>
</div>
</div>
)
}
/**
*
* #param {IConfirmationBody} body
* #param {IConfirmationDialogConfig} config
* #returns
*/
const getUserConfirmation = (
body: IConfirmationBody,
config?: IConfirmationDialogConfig
): Promise<{ value: string }> =>
new Promise((resolve) => {
renderDialog(body, resolve, config)
})
export default getUserConfirmation
Usage
// only one simple example
const UnsavedChangesDialog: TConfirmationDialog = {
body: {
icon: <svg>ICON CODE OPTIONAL</svg>,
title: "Your title",
message:
"Your message",
},
config: {
okButtonText: "Yes",
cancelButtonText: "No",
title: "Unsaved changes",
isDeleteConfirmation: true,
bodyClassName: "px-[15vw]",
},
}
const hasChanges = useRef(false)
useWarnIfUnsaved(hasChanges.current, UnsavedChangesDialog)
const handleFormChange = (e: ChangeEvent<HTMLInputElement>) => {
hasChanges.current = true
...
}
const submitForm = (data) => {
...
if (success) {
// important to set as false when you actually submit so you can stop showing the message
hasChanges.current = false
}
}

Related

Key={id} not working in react while mapping

I am making a simple messaging app, when i try to send messages the same div gets updated with new message but a new div does not get added. i was using index first while mapping the messages to display but it did not work so i added a msgId to the messages and tried to use it as the key but it's still not working
this is OpenConversation.js
import React, { useState } from 'react'
import {Button, Form, InputGroup, ListGroup} from 'react-bootstrap'
import { useConversations } from '../contexts/ConversationsProvider'
export default function OpenConversation() {
const[text,setText]=useState()
const {sendMessage,selectedConversation}=useConversations()
function handleSubmit(e) {
e.preventDefault()
console.log("index",selectedConversation)
sendMessage(
selectedConversation.recipients.map(r => r.id),
text
)
setText('')
}
return (
<div className="d-flex flex-column flex-grow-1">
<div className="flex-grow-1 overflow-auto">
<div className="d-flex flex-column align-items-start justify-content-end px-3">
{selectedConversation.messages.map((message) => {
return (
<div
key={message.msgId}
className={`my-1 d-flex flex-column ${message.fromMe ? 'align-self-end align-items-end' : 'align-items-start'}`}
>
<div
className={`rounded px-2 py-1 ${message.fromMe ? 'bg-primary text-white' : 'border'}`}>
{message.text} {message.msgId}
</div>
<div className={`text-muted small ${message.fromMe ? 'text-right' : ''}`}>
{message.fromMe ? 'You' : message.senderName}
</div>
</div>
)
})}
</div>
</div>
<Form style={{position:'absolute',bottom:'0rem'}}
onSubmit={handleSubmit}
>
<Form.Group className='m-2'>
<InputGroup >
<Form.Control as='textarea' required
value={text}
onChange={e=>setText(e.target.value)}
style={{height:'75PX',resize:'none',}}
/>
<Button type="submit" style={{background:'#7c73e6',border:'none'}}>Send</Button>
</InputGroup>
</Form.Group>
</Form>
</div>
)
}
ConversationsProvider.js
import React, { useContext, useState, useEffect, useCallback } from 'react'
import useLocalStorage from '../hooks/useLocalStorage';
import { useContacts } from './ContactsProvider';
import {v4 as uuidv4} from 'uuid'
const ConversationsContext = React.createContext()
export function useConversations() {
return useContext(ConversationsContext)
}
export default function ConversationsProvider({ id, children }) {
const [conversations, setConversations] = useLocalStorage('conversations', [])
const [selectedConversationIndex,setSelectedConversationIndex]=useState(0)
const { contacts } = useContacts()
function createConversation(recipients) {
setConversations(prevConversations => {
return [...prevConversations, { recipients, messages: [] }]
})
}
function selectConversationIndex(i)
{
setSelectedConversationIndex(i)
}
function addMessageToConversation({recipients,text,sender})
{
setConversations(prevConversations=>{
let madeChange=false
let msgId=uuidv4()
const newMessage={sender,text,msgId}
const newConversations=prevConversations.map(conversation=>{
if(arrayEquality(conversation.recipients,recipients))
{
madeChange=true
return{
...conversation,
messages:[conversation.messages,newMessage]
}
}
return conversation
})
if(madeChange){
return newConversations
}
else{
return [...prevConversations,{recipients,messages:[newMessage]}]
}
})
console.log(conversations)
}
function sendMessage(recipients,text)
{ console.log("working")
addMessageToConversation({recipients,text,sender:id})
}
const formattedConversations = conversations.map((conversation, index) => {
const recipients = conversation.recipients.map(recipient => {
const contact = contacts.find(contact => {
return contact.id === recipient
})
const name = (contact && contact.name) || recipient
return { id: recipient, name }
})
const messages = conversation.messages.map(message => {
const contact = contacts.find(contact => {
return contact.id === message.sender
})
const name = (contact && contact.name) || message.sender
const fromMe = id === message.sender
return { ...message, senderName: name, fromMe }
})
const selected = index === selectedConversationIndex
return { ...conversation, messages, recipients, selected }
})
const value = {
conversations: formattedConversations,
selectedConversation: formattedConversations[selectedConversationIndex],
sendMessage,
selectConversationIndex: setSelectedConversationIndex,
createConversation
}
return (
<ConversationsContext.Provider value={value}>
{children}
</ConversationsContext.Provider>
)
}
function arrayEquality(a, b) {
console.log("working")
if (a.length !== b.length) return false
a.sort()
b.sort()
return a.every((element, index) => {
return element === b[index]
})
}
i did not understand why index did not work, is there another way to map the messages?
Your error comes from this line
messages:[conversation.messages,newMessage]
in the addMessageToConversation function
I think you need to spread conversation.messages.
Change [conversation.messages, newMessage] to [...conversation.messages, newMessage].
With what you're doing currently, you're making the previous messages object, the first item in the array and then the new one as the second item.

Ionic Capacitor App eventlistner plugin called multiple times in a single click

I am using Ionic capacitor to build my reactjs Web application. I have added a Capacitor App Apis to get the back event capture in my App.js file like this,
APPS.addListener("backButton", () => {
if (renderedScreenUIList.length === 0) {
// APPS.exitApp();
alert("exit app");
} else {
alert("previous screen");
}
});
But when i press back button this back event is called 3 times instead of one. my App.js file is.
// import "./StyleSeets/LightTheme.css";
import "./StyleSeets/theme.css";
import "./App.css";
import React, { useEffect, useState } from "react";
import Drawer from "#mui/material/Drawer";
import { App as APPS } from "#capacitor/app";
import Login from "./Components/Login";
import ERPUtils from "./Components/ERPUtils";
import Main from "./Components/Main";
// ************************** back fire event handling ****************
const renderedScreenUIList = [];
function App() {
// const [ stylePath, setStylePath ] = useState("./StyleSeets/LightTheme.css");
const [renderPageType, setRenderPageType] = useState("login");
const [isMobileView, setIMobileView] = useState(window.innerWidth < 920);
const isConsoleOpen = false;
window.onresize = function () {
console.log(window.outerHeight - window.innerHeight);
if ((window.outerHeight - window.innerHeight) > 100) { console.log("open"); } else console.log("close");
};
const themeList = [
{ value: "#2776ed", label: "light" },
{ value: "#45b11c", label: "nature" },
{ value: "#2AB67B", label: "evening" },
{ value: "#add8e6", label: "sky" },
{ value: "#2b035b", label: "dark" }];
const languageList = [
{ value: "ENG", label: "English" },
{ value: "MAL", label: "മലയാളം" },
{ value: "HIND", label: "हिन्दी" }];
const [language, setLanguage] = useState("ENG");
const onThemeChangeCall = () => {
const themeName = sessionStorage.getItem("theme-name");
const themeElement = document.getElementById("app-theme");
themeElement.className = "App";
document.documentElement.setAttribute("data-theme", themeName);
};
APPS.addListener("backButton", () => {
if (renderedScreenUIList.length === 0) {
// APPS.exitApp();
alert("exit app");
} else {
alert("previous screen");
}
});
const languageChanged = (type) => {
setLanguage(type);
};
useEffect(() => {
onThemeChangeCall();
}, []);
const changePage = (page) => {
setRenderPageType(page);
};
const [isSettingsOpen, setIsSettingsOpen] = useState(false);
const changeTheme = (type) => {
sessionStorage.setItem("theme-name", type);
document.documentElement.classList.add("color-theme-in-transition");
window.setTimeout(() => {
document.documentElement.classList.remove("color-theme-in-transition");
}, 1000);
onThemeChangeCall();
};
const toggleDrawer = (value) => {
setIsSettingsOpen(value);
};
const changeLanguage = (type) => {
sessionStorage.setItem("language-theme", type);
languageChanged(type);
};
useEffect(() => {
setIMobileView(window.innerWidth < 920);
}, [window.innerWidth]);
return (
<div className="App" id="app-theme">
{renderPageType === "login"
? (
<Login
onThemeChangeCall={onThemeChangeCall}
changePage={changePage}
languageChanged={languageChanged}
toggleDrawer={toggleDrawer}
/>
)
: null}
{renderPageType === "home"
? (
<Main
onThemeChangeCall={onThemeChangeCall}
changePage={changePage}
languageChanged={languageChanged}
toggleDrawer={toggleDrawer}
isMobileView={isMobileView}
/>
)
: null}
<Drawer
anchor="right"
open={isSettingsOpen}
onClose={() => toggleDrawer(false)}
>
<div className="p-2 " style={{ width: "250px", backgroundColor: "var(--primaryBackground)" }}>
<h6 className="border-bottom p-2">Themes</h6>
<div className="">
{!ERPUtils.isNullorWhiteSpace(themeList) ? themeList.map((el, index) => (
<div
key={index}
onKeyDown={(e) => {
if (ERPUtils.isKeyBoardEnterPressed(e)) {
changeTheme(el.label);
}
}}
role="button"
tabIndex={0}
className="p-2 d-flex align-items-center font-2 justify-content-start"
onClick={() => changeTheme(el.label)}
>
<span className="theme-thumbnail" style={{ backgroundColor: el.value }} />
<span>{(`${el.label}`).toLocaleUpperCase()}</span>
</div>
)) : null}
</div>
</div>
<div className="p-2" style={{ width: "250px", backgroundColor: "var(--primaryBackground)" }}>
<h6 className="border-bottom p-2">Language</h6>
<div className="">
{!ERPUtils.isNullorWhiteSpace(languageList) ? languageList.map((el, index) => (
<div
key={index}
onKeyDown={(e) => {
if (ERPUtils.isKeyBoardEnterPressed(e)) {
changeLanguage(el.value);
}
}}
role="button"
tabIndex={0}
className="p-2 d-flex align-items-center font-2 justify-content-start"
onClick={() => changeLanguage(el.value)}
>
<span className="language-thumbnail">{el?.label.split("")[0]}</span>
<span>{(`${el.label}`).toLocaleUpperCase()}</span>
</div>
)) : null}
</div>
</div>
</Drawer>
{/* <ThemeSwitcherProvider defaultTheme="light" themeMap={themes}> */}
{/* <link rel="stylesheet" type="text/css" href={stylePath} /> */}
{/* </ThemeSwitcherProvider> */}
</div>
);
}
export default App;
Kindly suggest a solution for this problem ?. If I call this event listener outside App.js file it only calls once.
With each page reload the listener is added again. To fix this you should call removeAllListeners() => Promise<void> once at the beginning:
App.removeAllListeners().then(() => {
App.addListener("backButton", () => {
if (renderedScreenUIList.length === 0) {
// APPS.exitApp();
alert("exit app");
} else {
alert("previous screen");
}
});
});

React detect click outside component clearing my state

I have an outsideAlerter component that functions elsewhere on my site. I am now using it on a repeatable component and for some reason it is clearing my state effectively breaking my desired outcome.
below is my wrapper component that detects if you click outside of its children
import React, { useRef, useEffect } from "react";
/**
* Hook that alerts clicks outside of the passed ref
*/
function useOutsideAlerter(ref, onClickOutside) {
useEffect(() => {
/**
* Alert if clicked on outside of element
*/
function handleClickOutside(event) {
if (ref.current && !ref.current.contains(event.target)) {
//console.log(onClickOutside);
onClickOutside();
}
}
// Bind the event listener
document.addEventListener("mousedown", handleClickOutside);
return () => {
// Unbind the event listener on clean up
document.removeEventListener("mousedown", handleClickOutside);
};
}, []);
}
/**
* Component that alerts if you click outside of it
*/
export default function OutsideAlerter(props) {
const wrapperRef = useRef(null);
useOutsideAlerter(wrapperRef, props.onClickOutside);
return <div ref={wrapperRef}>{props.children}</div>;
}
Below is my controller component, it handles state
const TableFilterDropdownController = ({style, rows, colKey, activeFilters, addActiveFilter}) => {
const [tableFilterState, setTableFilterState] = useState(
{
state: INACTIVE,
iconColor: "black",
filter: "",
filteredRows: [...rows],
localActiveFilters: []
}
);
useEffect(() => {
let state = tableFilterState.state;
let localActiveFilters = tableFilterState.localActiveFilters;
if (state === INACTIVE && localActiveFilters.length > 0) {
setTableFilterState({...tableFilterState, state: ACTIVE})
}
}, [tableFilterState.state])
//filter out repeats and rows that don't match input
useEffect(() => {
let filter = tableFilterState.filter
if (filter !== "") {
let tempFilteredRows = [];
rows.map(row => {
if (row[colKey].toLowerCase().includes(filter.toLowerCase()) &&
!tempFilteredRows.includes(row[colKey])) {
tempFilteredRows.push(row[colKey]);
}
})
setTableFilterState({...tableFilterState, filteredRows: tempFilteredRows})
}
else {
let tempFilteredRows = [];
rows.map(row => {
if (!tempFilteredRows.includes(row[colKey])) {
tempFilteredRows.push(row[colKey]);
}
})
setTableFilterState({...tableFilterState, filteredRows: tempFilteredRows});
}
}, [tableFilterState.filter, rows])
const onClick = () => {
if (tableFilterState.state === DROP_DOWN) {
console.log(tableFilterState)
if (tableFilterState.localActiveFilters.length > 0) {
//setState(ACTIVE)
setTableFilterState({...tableFilterState, state: ACTIVE});
}
else {
//setState(INACTIVE)
setTableFilterState({...tableFilterState, state: INACTIVE});
}
}
else {
//setState(DROP_DOWN)
setTableFilterState({...tableFilterState, state: DROP_DOWN});
}
}
//something here is breaking it and resetting on click outside
const onClickOutside = () => {
setTableFilterState({...tableFilterState, state: INACTIVE});
}
let addLocalActiveFilter = (filter) => {
let newActiveFilters = [...tableFilterState.localActiveFilters];
const index = newActiveFilters.indexOf(filter);
if (index > -1) {
newActiveFilters.splice(index, 1);
} else {
newActiveFilters.push(filter);
}
setTableFilterState({...tableFilterState, localActiveFilters: newActiveFilters});
}
return (
<TableFilterDropdown
style={style}
color={tableFilterState.iconColor}
state={tableFilterState.state}
onClick={onClick}
onClickOutside={onClickOutside}
dropLeft={true}
filter={tableFilterState.filter}
setFilter={e => setTableFilterState({...tableFilterState, filter: e.target.value})}
>
{tableFilterState.filteredRows.map((item, index) => {
return (
<CheckboxInput
value={item}
label={item}
key={index}
onChange={e => {
addActiveFilter(e.target.value);
addLocalActiveFilter(e.target.value)
}}
isChecked={tableFilterState.localActiveFilters.includes(item)}
/>
);
})}
</TableFilterDropdown>
);
}
export default TableFilterDropdownController;
And lastly below is the UI component
const TableFilterDropdown = ({style, state, color, children, onClick, onClickOutside, dropLeft, filter, setFilter}) => {
useEffect(() => {
console.log("state change")
console.log(state);
}, [state])
return (
<div
className={`sm:relative inline-block ${style}`}
>
<OutsideAlerter onClickOutside={onClickOutside}>
<IconButton
type="button"
style={`relative text-2xl`}
onClick={onClick}
>
<IconContext.Provider value={{color: color}}>
<div>
{state === DROP_DOWN ?
<AiFillCloseCircle /> :
state === ACTIVE ?
<AiFillFilter /> :
<AiOutlineFilter />
}
</div>
</IconContext.Provider>
</IconButton>
{state === DROP_DOWN ?
<div className={`flex flex-col left-0 w-screen sm:w-32 max-h-40 overflow-auto ${dropLeft ? "sm:origin-top-left sm:left-[-2.5rem]" : "sm:origin-top-right sm:right-0"} absolute mt-2 rounded-md shadow-lg bg-white ring-1 ring-black ring-opacity-5 focus:outline-none z-10`} role="menu" aria-orientation="vertical" aria-labelledby="menu-button">
<SearchBar label={"Search"} placeholder={"Search"} value={filter} onChange={setFilter} />
{children}
</div>
: null}
</OutsideAlerter>
</div>
);
For some reason whenever you click outside the component the tableFilterState gets set to
{
state: INACTIVE,
iconColor: "black",
filter: "",
filteredRows: [],
localActiveFilters: []
}
Which is not intentional, the tableFilterState should stay the same, only state should change. I can't figure this out so please help!!!
When you call useOutsideAlerter and pass onClickOutside handler it captures tableFilterState value and use it in a subsequent calls. This is a stale state. You could try this approach or use refs as described in docs:
const onClickOutside = () => {
setTableFilterState(tableFilterState => ({
...tableFilterState,
state: INACTIVE,
}));
}

How can i stop this infinite loop in my next.js app? (Search page via url params)

I want to create a search page that in driven by the url query. It includes params such as:
searchQuery e.g. "nike shoes"
page e.g. "1"
etc.
However im struggling at the moment with getting it work without heading into an infinite loop.
Here is my search page:
function Search() {
const [listings, setListings] = useState<null | Array<ListingObject>>(null);
const [noResultsListings, setNoResultsListings] = useState<
Array<ListingObject>
>([]);
const [loading, setLoading] = useState("idle");
const [mobileFiltersOpen, setMobileFiltersOpen] = useState(false);
const [paginationData, setPaginationData] = useState<PaginationData>({
totalHits: 0,
totalPages: 0,
});
const { query } = Router;
function onFilterCheck(checked: Boolean, id: string, option) {
let query = Router.query;
if (checked) {
if (query[id]) {
query[id] += `,${option.value}`;
} else {
query[id] = `${option.value}`;
}
}
if (!checked) {
if (query[id] && query[id].toString().split(",").length > 1) {
let param = query[id].toString();
let array = param.split(",");
query[id] = array.filter((param) => param !== option.value);
} else {
delete query[id];
}
}
Router.push(
{
pathname: `/search`,
query,
},
undefined,
{ shallow: true }
);
}
const onSortByChanged = (sortByIndex: string) => {
let { query } = Router;
if (sortByIndex) {
query.sortBy = sortByIndex;
Router.push(
{
pathname: `/search`,
query,
},
undefined,
{ shallow: true }
);
}
};
const search = useCallback(async (query) => {
console.log("in search");
if (loading === "idle") {
console.log("inside IF");
setLoading("loading");
const readyQuery = await handleSearchQuery(query);
let listingIndex = searchClient.initIndex(query.sortBy ?? "listings");
const response = await listingIndex.search(readyQuery.searchQuery, {
numericFilters: readyQuery.filters.numericFilters,
facetFilters: readyQuery.filters.facetFilters,
page: query.page - 1 ?? 0,
});
if (response) {
const hits = response.hits;
setPaginationData({
totalPages: response.nbPages,
totalHits: response.nbHits,
});
if (hits.length === 0) {
const response = await getNewListings(3);
if (response.success) {
setNoResultsListings(response.listings);
}
}
setListings(hits);
}
setLoading("idle");
}
}, []);
const handlePageClick = useCallback(
(pageNumber) => {
console.log("page number from handlePageClick", pageNumber);
if (loading === "idle") {
let { query } = Router;
query.page = (parseInt(pageNumber) + 1).toString();
Router.push(
{
pathname: `/search`,
query,
},
undefined,
{ shallow: true }
);
search(query);
window.scrollTo(0, 0);
}
},
[loading, search]
);
useEffect(() => {
search(query);
}, [search, query]);
useEffect(() => {
if (process.env.NODE_ENV === "production") {
hotjar.initialize(
parseInt(process.env.NEXT_PUBLIC_HOTJAR_ID),
parseInt(process.env.NEXT_PUBLIC_HOTJAR_SV)
);
}
}, []);
if (!Router.query.page) {
const { query } = Router;
query.page = "1";
Router.push({ pathname: "/search", query }, undefined, {
shallow: true,
});
}
return (
<main className="mx-auto w-full transition-all duration-200 ease-in md:px-0">
<section className="flex w-full flex-col pt-0 md:flex-row">
<div className="hidden min-h-screen w-1/5 grid-cols-1 md:grid lg:grid-cols-1 lg:pr-4">
{/* Desktop filters */}
<form className="hidden h-full overflow-y-auto p-0 md:inline-block">
<div className="relative z-30 flex h-full flex-col border-r pb-6 pr-6 md:z-0">
<SearchSortBy onSortByChanged={onSortByChanged} />
<SearchFilters
mobileFiltersOpen={mobileFiltersOpen}
setMobileFiltersOpen={() => {
setMobileFiltersOpen(false);
}}
onFilterCheck={onFilterCheck}
/>
</div>
</form>
</div>
<span className="flex items-center justify-between">
<SearchSortBy
className="md:hidden"
onSortByChanged={onSortByChanged}
/>
<Button
className="!px-4 md:hidden"
variant="none"
type="button"
onClick={() => {
setMobileFiltersOpen(true);
}}
>
<FilterIcon className="w-4 text-blue-600" />
</Button>
</span>
<SearchResultsGrid
listings={listings}
noResultsListings={noResultsListings}
/>
</section>
{listings?.length > 0 && (
<SearchPagination
pageNumber={parseInt(Router?.query?.page.toString()) - 1}
paginationData={paginationData}
handlePageClick={handlePageClick}
/>
)}
</main>
);
}
export default Search;
If I search something on my site, the console will look like this:
in search
Search.tsx?c6ec:94 inside IF
Search.tsx?c6ec:92 in search
Search.tsx?c6ec:94 inside IF
Search.tsx?c6ec:92 in search
Search.tsx?c6ec:92 in search
Search.tsx?c6ec:94 inside IF
Search.tsx?c6ec:92 in search
Search.tsx?c6ec:92 in search
Search.tsx?c6ec:94 inside IF
Search.tsx?c6ec:92 in search
Search.tsx?c6ec:92 in search
Search.tsx?c6ec:94 inside IF
Search.tsx?c6ec:92 in search
Search.tsx?c6ec:92 in search
Search.tsx?c6ec:94 inside IF
Search.tsx?c6ec:92 in search
Search.tsx?c6ec:92 in search
Search.tsx?c6ec:94 inside IF
I tried using loading as a gate, where it will only search if it is equal to idle, however I change the the loading state at the end of the search back to idle, therefore it triggers a loop.
Does anybody have any experience in how I will handle this? Is adding the router query to a useEffect to fire the search the right way to go? Or should I just pass the search function to all the things that need it?

ReactJS error TypeError: Cannot convert undefined or null to object

The affected code is (Typescript errors in local) (componet is correct, the name is as that):
{template.elements.map((element: TemplateElementModel, i) => {
const stand = roomStands?.find(
(stand: ExhibitorModel) => stand.standNumber === `${i + 1}`
);
const componet = stand ? (
<img
src={stand?.logo}
alt={`${stand?.name} logo`}
className="w-24 object-contain"
/>
) : undefined;
const handleClick = stand
? () => {
dispatch(setExhibitor(stand?.id));
dispatch(pushScreen(EventDetailScreen.stand));
}
: undefined;
That's when the code has an error but is just the new registers. When all is ok it shows the view but if has that error the view was a blank page.
I've tried a lot of solutions but i cannot detect what is the problem.
The complete code is:
import React, { useEffect, useState } from "react";
import {
TemplateElementModel,
TemplateModel,
} from "../../template/template.model";
import {
currentEventIdSelector,
roomIdSelector as eventRoomSelector,
popScreen,
pushScreen,
setExhibitor,
setRoom,
} from "../event.slice";
import { useDispatch, useSelector } from "react-redux";
import { Collections } from "../../../config/app.collections";
import DeimosModal from "../../../lib/deimos-ui/components/deimos-modal/DeimosModal";
import { EventDetailScreen } from "../enums/event-detail-screen";
import { ExhibitorModel } from "../../exhibitor/exhibitor.model";
import { RoomModel } from "../../room/room.model";
import { RootState } from "../../../app.store";
import TemplateMarker from "../../template/components/TemplateMarker";
import { TemplateTypes } from "../../template/template-types.enum";
import { useFirestoreConnect } from "react-redux-firebase";
interface EventExhibitionsPageProps {}
const EventExhibitionsPage: React.FC<EventExhibitionsPageProps> = () => {
const dispatch = useDispatch();
const [template, setTemplate] = useState<TemplateModel>();
const [roomStands, setRoomStands] = useState<ExhibitorModel[]>();
const [showRoomsModal, setShowRoomsModal] = useState(true);
const currentRoom = useSelector(eventRoomSelector);
const eventId = useSelector(currentEventIdSelector);
const rooms = useSelector(
(state: RootState) => state.firestore.data[Collections.ROOM]
);
const templates = useSelector(
(state: RootState) => state.firestore.data[Collections.TEMPLATE]
);
const stands = useSelector(
(state: RootState) => state.firestore.data[Collections.EXHIBITOR]
);
useFirestoreConnect([
{
collection: Collections.ROOM,
where: ["event", "==", eventId],
},
{
collection: Collections.TEMPLATE,
where: ["type", "==", TemplateTypes.Room],
},
{
collection: Collections.EXHIBITOR,
where: ["eventId", "==", eventId],
},
]);
useEffect(() => {
if (!currentRoom) return;
const template = templates[currentRoom?.template];
if (template) setTemplate(template);
}, [currentRoom, templates]);
useEffect(() => {
if (!currentRoom) return;
const filteredStands = Object.values<ExhibitorModel>(stands).filter(
(stand) => stand.room === currentRoom.id
);
setRoomStands(filteredStands);
}, [currentRoom, stands]);
return (
<div className="h-full w-full relative bg-no-repeat bg-center text-center">
<DeimosModal
isOpen={showRoomsModal}
hideCancelButton={!currentRoom}
closeButtonText="Salir"
cancelButtonText={`Seguir en ${currentRoom?.name}`}
handleCancel={() => {
setShowRoomsModal(false);
}}
handleClose={() => {
dispatch(popScreen());
}}
>
{!!rooms && (
<div>
<p className="font-bold">Selecciona una sala a la que entrar</p>
{Object.values<RoomModel>(rooms).map((room) => {
return (
<div
className="flex flex-row w-full my-2 p-3 bg-gray-200 rounded cursor-pointer"
onClick={() => {
dispatch(setRoom(room));
setShowRoomsModal(false);
}}
>
{room.name}
</div>
);
})}
</div>
)}
</DeimosModal>
{currentRoom && template && (
<div
className="w-full h-full flex flex-row justify-center align-center"
style={{
backgroundImage: `url(${template.background})`,
backgroundSize: "100% 100%",
}}
>
{template.elements.map((element: TemplateElementModel, i) => {
const stand = roomStands?.find(
(stand: ExhibitorModel) => stand.standNumber === `${i + 1}`
);
const componet = stand ? (
<img
src={stand?.logo}
alt={`${stand?.name} logo`}
className="w-24 object-contain"
/>
) : undefined;
const handleClick = stand
? () => {
dispatch(setExhibitor(stand?.id));
dispatch(pushScreen(EventDetailScreen.stand));
}
: undefined;
return (
<TemplateMarker
// backgroundColor="#327ba8"
width={element.w}
height={element.h}
x={element.x}
y={element.y}
key={element.id}
skewX={element.skewX ?? template.skewX}
skewY={element.skewY ?? template.skewY}
labelComponent={componet}
onClick={handleClick}
/>
);
})}
</div>
)}
</div>
);
};
export default EventExhibitionsPage;
TemplateMarker might not accept labelComponent to be undefined or null. You can add an extra check before sending TemplateMarker.
return componet && (
<TemplateMarker
// backgroundColor="#327ba8"
width={element.w}
height={element.h}
x={element.x}
y={element.y}
key={element.id}
skewX={element.skewX ?? template.skewX}
skewY={element.skewY ?? template.skewY}
labelComponent={componet}
onClick={handleClick}
/>
);

Categories

Resources