I have a modal component in my Next.js project using Radix UI. There are different two way binded inputs using state. The problem is when the state is update the entire page re-renders causing all state to be refreshed. Here is my code:
/* eslint-disable */
"use client";
import * as Dialog from "@radix-ui/react-dialog";
import * as Accordion from "@radix-ui/react-accordion";
import * as sanitizeHtml from "sanitize-html";
import "./components.scss";
import { SetStateAction, useEffect, useRef, useState } from "react";
import useFetch from "@/hooks/useFetch";
import HTTPMethod from "http-method-enum";
import Image from "next/image";
import { loading as loadingImg } from "@/public/icons";
import { Review } from "@/mongo/functions/review";
import { User } from "@/mongo/functions/user";
import Link from "next/link";
import Editor from "react-simple-wysiwyg";
import { expandLess } from "@/public/icons";
export default async function CreateReview({
userProp,
type,
currentUserProp,
existingReviewProp,
isSelf,
}: {
userProp: string;
type: string;
currentUserProp: string;
existingReviewProp: string;
isSelf: boolean;
}) {
const existingReview: Review = JSON.parse(existingReviewProp);
const currentUser: User = JSON.parse(currentUserProp);
const user: User = JSON.parse(userProp);
const titleRef = useRef("");
const [stars, setStars] = useState(5);
const [html, setHtml] = useState("");
function onChange(e) {
setHtml(sanitizeHtml(e.target.value));
}
const [wizardText, setWizardText] = useState([]);
const [wizardOpen, setWizardOpen] = useState(true)
const [data, setData] = useState(null);
const [error, setError] = useState("");
const [loading, setLoading] = useState(false);
const [deleyed, setDeleyed] = useState(false);
const [aborted, setAborted] = useState(false);
useEffect(() => {
if (!error && !loading && data) {
window.location.reload();
}
}, [data, error, loading]);
const onSubmit = async (e: any) => {
e.preventDefault();
useFetch(setData, setError, setLoading, setDeleyed, aborted, {
url: "/api/reviews/add",
method: HTTPMethod.POST,
body: {
title: titleRef.current.value,
stars: stars,
text: html,
type: type,
author: currentUser._id,
recipient: user._id,
status: "unlisted",
},
});
};
const onChecked = (e: any, text: string, id: string) => {
const wizardTextArray = [...wizardText];
if (e.target.checked) {
setHtml(html + text);
} else if (!e.target.checked) {
const htmlRegex = new RegExp(
`<span\s+id=['"]${id}['"][^>]*>.*?<\/span>`,
"g"
);
console.log(htmlRegex);
console.log(html.replace(htmlRegex, ""));
setHtml(html.replace(htmlRegex, ""));
}
};
if (isSelf) {
return (
<>
<h1>You can't write a review for yourself.</h1>
</>
);
}
if (existingReview) {
return (
<>
<h1>You've already writen review for this user.</h1>
</>
);
}
if (!currentUser.businessName) {
return (
<>
<h1>Setup your business profile to start rating</h1>
<button className="standard-clr-btn">
<Link href="/app/business">Setup</Link>
</button>
</>
);
}
const ReviewWizard = () => {
return (
<Accordion.Root
className="AccordionRoot"
type="single"
defaultValue="item-1"
collapsible
>
<Accordion.Item className="AccordionItem" value="item-1">
<Accordion.Trigger>
Review Wizard (Beta)
<span className="toggle-open"> Toggle Open</span>
</Accordion.Trigger>
<Accordion.Content>
{stars === 5 ? (
<ul>
<li>
<input
type="checkbox"
onClick={(e) =>
onChecked(
e,
`<span id="great-work-with">Great to work with! </span>`,
"great-work-with"
)
}
/>
<span>Great to work with! </span>
</li>
<li>
<input
type="checkbox"
onClick={(e) =>
onChecked(
e,
`<span id="loyal-customer">The customer is loyal. </span>`,
"loyal-customer"
)
}
/>
<span>The customer is loyal. </span>
</li>
<li>
<input
type="checkbox"
onClick={(e) =>
onChecked(
e,
`<span id="payed-on-time">The customer payed on time. </span>`,
"payed-on-time"
)
}
/>
<span>The customer payed on time. </span>
</li>
</ul>
) : null}
{stars === 4 ? (
<ul>
<li>
<input
type="checkbox"
onClick={(e) =>
onChecked(
e,
`<span id="loyal-customer">The customer is loyal. </span>`,
"loyal-customer"
)
}
/>
<span>The customer is loyal. </span>
</li>
<li>
<input
type="checkbox"
onClick={(e) =>
onChecked(
e,
`<span id="payed-on-time">The customer paid on time. </span>`,
"payed-on-time"
)
}
/>
<span>The customer payed on time. </span>
</li>
<li>
<input
type="checkbox"
onClick={(e) =>
onChecked(
e,
`<span id="below-average-conditions">The work conditions on the property were below average. </span>`,
"below-average-conditions"
)
}
/>
<span>
The work conditions on the property were below average.
</span>
</li>
</ul>
) : null}
{stars === 3 ? (
<ul>
<li>
<input
type="checkbox"
onClick={(e) =>
onChecked(
e,
`<span id="disloyal-customer">The customer is disloyal. </span>`,
"disloyal-customer"
)
}
/>
<span>The customer is disloyal. </span>
</li>
<li>
<input
type="checkbox"
onClick={(e) =>
onChecked(
e,
`<span id="fault-finding-customer">The customer is fault finding. </span>`,
"fault-finding-customer"
)
}
/>
<span>The customer is fault finding. </span>
</li>
<li>
<input
type="checkbox"
onClick={(e) =>
onChecked(
e,
`<span id="late-pay">The customer did not pay on time. </span>`,
"late-pay"
)
}
/>
<span>The customer did not pay on time. </span>
</li>
<li>
<input
type="checkbox"
onClick={(e) =>
onChecked(
e,
`<span id="payed-on-time">The customer paid on time. </span>`,
"payed-on-time"
)
}
/>
<span>The customer payed on time. </span>
</li>
<li>
<input
type="checkbox"
onClick={(e) =>
onChecked(
e,
`<span id="below-average-conditions">The work conditions on the property were below average. </span>`,
"below-average-conditions"
)
}
/>
<span>
The work conditions on the property were below average.
</span>
</li>
</ul>
) : null}
{stars === 2 ? (
<ul>
<li>
<input
type="checkbox"
onClick={(e) =>
onChecked(
e,
`<span id="disloyal-customer">The customer is disloyal. </span>`,
"disloyal-customer"
)
}
/>
<span>The customer is disloyal. </span>
</li>
<li>
<input
type="checkbox"
onClick={(e) =>
onChecked(
e,
`<span id="fault-finding-customer">The customer is fault finding. </span>`,
"fault-finding-customer"
)
}
/>
<span>The customer is fault finding. </span>
</li>
<li>
<input
type="checkbox"
onClick={(e) =>
onChecked(
e,
`<span id="late-pay">The customer did not pay on time. </span>`,
"late-pay"
)
}
/>
<span>The customer did not pay on time. </span>
</li>
<li>
<input
type="checkbox"
onClick={(e) =>
onChecked(
e,
`<span id="below-average-conditions">The work conditions on the property were below average. </span>`,
"below-average-conditions"
)
}
/>
<span>
The work conditions on the property were below average.
</span>
</li>
</ul>
) : null}
{stars === 1 ? (
<ul>
<li>
<input
type="checkbox"
onClick={(e) =>
onChecked(
e,
`<span id="disloyal-customer">The customer is disloyal. </span>`,
"disloyal-customer"
)
}
/>
<span>The customer is disloyal. </span>
</li>
<li>
<input
type="checkbox"
onClick={(e) =>
onChecked(
e,
`<span id="fault-finding-customer">The customer is fault finding. </span>`,
"fault-finding-customer"
)
}
/>
<span>The customer is fault finding. </span>
</li>
<li>
<input
type="checkbox"
onClick={(e) =>
onChecked(
e,
`<span id="no-pay">The customer did not pay. </span>`,
"no-pay"
)
}
/>
<span>The customer did not pay. </span>
</li>
<li>
<input
type="checkbox"
onClick={(e) =>
onChecked(
e,
`<span id="below-average-conditions">The work conditions on the property were below average. </span>`,
"below-average-conditions"
)
}
/>
<span>
The work conditions on the property were below average.
</span>
</li>
</ul>
) : null}
</Accordion.Content>
</Accordion.Item>
</Accordion.Root>
);
};
return (
<Dialog.Root>
<Dialog.Trigger asChild>
<button className="standard-clr-btn" style={{ height: 50 }}>
Write a review
</button>
</Dialog.Trigger>
<Dialog.Portal>
<Dialog.Overlay className="DialogOverlay" />
<Dialog.Content className="DialogContent standard-dialog">
{currentUser.isBetaUser ? <ReviewWizard /> : null}
{loading && !error ? (
<>
<Image
src={loadingImg}
alt="Loading..."
className="popup-loading"
/>
{deleyed ? (
<div>
<h1>Your request is taking longer than usual</h1>
<p>
We are still working to complete it. You can cancel this
request and try again if you like. Note you will lose any
data you submited.
</p>
<button
onClick={() => setAborted(true)}
className="standard-clr-btn"
>
Cancel
</button>
</div>
) : null}
</>
) : (
<form onSubmit={(e) => onSubmit(e)}>
{error ? <span className="error-text">{error} </span> : null}
<label>How many stars?</label>
<select
value={stars}
onChange={(e) => setStars(Number(e.target.value))}
className="standard-dropdown"
>
<option value={1}>1 star</option>
<option value={2}>2 stars</option>
<option value={3}>3 stars</option>
<option value={4}>4 stars</option>
<option value={5} selected>
5 stars
</option>
</select>
<label>What's your title?</label>
<input
className="standard-input"
name="title"
ref={titleRef}
placeholder="Great to work with!"
/>
<label>What do you want to say?</label>
<Editor value={html} onChange={onChange} />
<button
className="standard-clr-btn"
aria-label="Send"
type="submit"
style={{ height: 50 }}
>
Publish
</button>
</form>
)}
<Dialog.Close asChild>
<button className="IconButton" aria-label="Close">
Close
</button>
</Dialog.Close>
</Dialog.Content>
</Dialog.Portal>
</Dialog.Root>
);
}
The strange thing is the refs aren’t causing the entire page to render just the areas with state.
2
Answers
The issue was the async part of the client component.
Using
window.location.reload();
in a single page application such as React.js defeats the purpose of why Single Page Applications were created in the first place, consider removing it. Any update to the following statesdata
,error
,loading
used as deps for theuseEffect
will cause your window to reload and this in turn resets all your application state.