So I have build a demo site with a form of submiting a new user.
The files:
App.jsx:
import React from 'react';
import './App.css';
import {
RouterProvider,
createBrowserRouter
} from 'react-router-dom';
//components
import Home from './pages-views/Home.jsx';
import AboutUs from './pages-views/AboutUs.jsx';
import Products from './pages-views/Products.jsx';
import Product from './pages-views/Product.jsx';
import Users from './pages-views/Users.jsx';
import UsersError from './pages-views/UsersError.jsx';
import User from './pages-views/User.jsx';
import UserError from './pages-views/UserError.jsx';
import NewUser from './pages-views/NewUser.jsx';
import ContactUs from './pages-views/ContactUs.jsx';
//layouts
import RootLayout from './components/RootLayout.jsx';
import ErrorLayout from './components/ErrorLayout.jsx';
//loaders
import { loadUsers, loadUser } from './loaders/loaders.jsx';
//actions
import { validateAndSubmitNewUser } from './actions/validateAndSubmitNewUser.jsx';
const myRouter = createBrowserRouter([
{
path: '/',
element: <RootLayout />,
//catches every error on every child Route that NOT HAVE a errorElement property
errorElement: <ErrorLayout />,
children: [
{
path: '/',
element: <Home />,
},
{
path: '/about-us',
element: <AboutUs />,
},
{
path: '/products',
element: <Products />,
},
{
path: '/product/:id',
element: <Product />,
},
{
path: '/users',
element: <Users />,
loader: loadUsers,
errorElement: <UsersError />,
},
{
path: '/user/:id',
element: <User />,
loader: loadUser,
//errorElement: <UserError />,
},
{
action: validateAndSubmitNewUser,
path: '/new-user',
element: <NewUser />,
},
{
path: '/contact-us',
element: <ContactUs />,
},
],
},
]);
export default function App() {
return (
<>
<RouterProvider router={myRouter} />
</>
);
}
MainBody.jsx:
import { Outlet, useRouteError } from 'react-router-dom';
export default function MainBody() {
let error = useRouteError();
const isError = (error != null);
console.log('is Error? ', isError);
console.log(error);
return (
<>
<Outlet />
{isError ? (
<>
<p>Error details:</p>
{/*printed when I throw new Error('User not found') on loader*/}
<p>{error.name}</p>
<p>{error.message}</p>
<p>{error.stack}</p>
<p>----------</p>
</>
) : (
<p></p>
)}
</>
);
}
Users.jsx:
import { Link, useLoaderData, useNavigation } from 'react-router-dom';
import UserCard from '../components/UserCard.jsx';
export default function Users() {
let pageState = useNavigation().state; //return idle | submitting | loading as value
console.log(pageState);
const users = useLoaderData();
return (
pageState === 'loading' ? (
<>
<p>Products page</p>
<p>Loading....</p>
</>
) : (
<>
<p>This is Users page</p>
<Link
to="/new-user"
class="btn btn-outline-primary"
style={{ margin: '8px' }}
>
Add new User
</Link>
{users.map((user) => (
<Link to={'/user/' + user.id} key={user.id}>
<UserCard name={user.name} userData={user} />
</Link>
))}
</>
)
);
}
validateAndSubmitNewUser.js (this is the action of Route path "/new-user")
export async function validateAndSubmitNewUser({ params, request }) {
let formUserData = await request.formData();
const user = {
name,
username,
email,
street,
streetNumber,
city,
zipcode,
phone,
website,
};
user.name = formUserData.get('name');
user.username = formUserData.get('username');
user.email = formUserData.get('email');
user.street = formUserData.get('street');
user.streetNumber = formUserData.get('street-number');
user.city = formUserData.get('city');
user.zipcode = formUserData.get('zipcode');
user.phone = formUserData.get('phone');
user.website = formUserData.get('website');
//variable validate will be the errors obj or null: that is what validateNewUser(user) returns
let validate = validateNewUser(user);//works fine
if (validate === null) {
//just a api call to https://jsonplaceholder.typicode.com/users/ to submit user
//returns the submited data as json
let data = await submitNewUser(user);
}
return validate;
}
NewUser.jsx (here is the problem)
import clsx from 'clsx';
import { Form, useActionData, useNavigate } from 'react-router-dom';
export default function NewUser() {
const navigate = useNavigate();
const errors = useActionData();
console.log('display errors ', errors);
//if there is not errors redirect-navigate to /users page
if (errors === null) {
console.log("navigate(/users)");
navigate("/users");//** (I think so) THIS IS RESPONSIBLE FOR THE PROBLEM-ERROR. See error stack below
}
return (
<Form method="post" action="/new-user">
<div className="input-group mb-4">
<span className="input-group-text" id="name">
Name
</span>
<input
type="text"
name="name"
className={clsx('form-control', errors?.name && 'is-invalid')}
aria-label="name input"
aria-describedby="nameFeedback"
/>
<div id="usernameFeedback" className="invalid-tooltip">
{errors?.name}
</div>
</div>
<div className="input-group mb-4">
<span className="input-group-text" id="username">
Username
</span>
<input
type="text"
name="username"
className={clsx('form-control', errors?.username && 'is-invalid')}
aria-label="username input"
aria-describedby="usernameFeedback"
/>
<div id="usernameFeedback" className="invalid-tooltip">
{errors?.username}
</div>
</div>
<div className="input-group mb-3">
<span className="input-group-text" id="email">
Email
</span>
<input
type="text"
name="email"
className={clsx('form-control', errors?.email && 'is-invalid')}
aria-label="Sizing example input"
aria-describedby="emailFeedback"
/>
<div id="emailFeedback" className="invalid-tooltip">
{errors?.email}
</div>
</div>
<div className="input-group mb-3">
<span className="input-group-text" id="street">
Street
</span>
<input
type="text"
name="street"
className={clsx('form-control', errors?.street && 'is-invalid')}
aria-label="Sizing example input"
aria-describedby="streetFeedback"
/>
<div id="streetFeedback" className="invalid-tooltip">
{errors?.street}
</div>
</div>
<div className="input-group mb-3">
<span className="input-group-text" id="street-number">
Number
</span>
<input
type="text"
name="street-number"
className={clsx('form-control', errors?.streetNumber && 'is-invalid')}
aria-label="Sizing example input"
aria-describedby="street-number-feedback"
/>
<div id="street-number-feedback" className="invalid-tooltip">
{errors?.streetNumber}
</div>
</div>
<div className="input-group mb-3">
<span className="input-group-text" id="city">
City
</span>
<input
type="text"
name="city"
className={clsx('form-control', errors?.city && 'is-invalid')}
aria-label="Sizing example input"
aria-describedby="cityFeedback"
/>
<div id="cityFeedback" className="invalid-tooltip">
{errors?.city}
</div>
</div>
<div className="input-group mb-3">
<span className="input-group-text" id="zipcode">
Zipcode
</span>
<input
type="text"
name="zipcode"
className={clsx('form-control', errors?.zipcode && 'is-invalid')}
aria-label="Sizing example input"
aria-describedby="zipcodeFeedback"
/>
<div id="zipcodeFeedback" className="invalid-tooltip">
{errors?.zipcode}
</div>
</div>
<div className="input-group mb-3">
<span className="input-group-text" id="phone">
Phone
</span>
<input
type="text"
name="phone"
className={clsx('form-control', errors?.phone && 'is-invalid')}
aria-label="Sizing example input"
aria-describedby="phoneFeedback"
/>
<div id="phoneFeedback" className="invalid-tooltip">
{errors?.phone}
</div>
</div>
<div className="input-group mb-3">
<span className="input-group-text" id="website">
Website
</span>
<input
type="text"
name="website"
className={clsx('form-control', errors?.website && 'is-invalid')}
aria-label="Sizing example input"
aria-describedby="websiteFeeback"
/>
<div id="websiteFeeback" className="invalid-tooltip">
{errors?.website}
</div>
</div>
<button type="submit" className="btn btn-outline-primary" name="submit">
Submit
</button>
</Form>
);
}
What I want to achieve is after a success validation and submition to be redirected to path "/users".
Now what happen is that the browser memory/processing looks to overflow, because of a rendering loop(I think so)
/*
Warning: Cannot update a component(`RouterProvider`) while rendering a different component(`NewUser`).
To locate the bad setState() call inside`NewUser`,
follow the stack trace as described in https://reactjs.org/link/setstate-in-render
NewUser@http://localhost:5173/src/pages-views/NewUser.jsx?t=1707325029056:22:20
RenderedRoute@http://localhost:5173/node_modules/.vite/deps/react-router-dom.js?v=6a7590ff:3550:7
Outlet@http://localhost:5173/node_modules/.vite/deps/react-router-dom.js?v=6a7590ff:3918:3
MainBody@http://localhost:5173/src/components/MainBody.jsx:21:15
RootLayout
RenderedRoute@http://localhost:5173/node_modules/.vite/deps/react-router-dom.js?v=6a7590ff:3550:7
RenderErrorBoundary@http://localhost:5173/node_modules/.vite/deps/react-router-dom.js?v=6a7590ff:3507:5
DataRoutes@http://localhost:5173/node_modules/.vite/deps/react-router-dom.js?v=6a7590ff:4660:7
Router@http://localhost:5173/node_modules/.vite/deps/react-router-dom.js?v=6a7590ff:3932:7
RouterProvider@http://localhost:5173/node_modules/.vite/deps/react-router-dom.js?v=6a7590ff:4477:7
App react-dom.development.js:86:29
*/
2
Answers
because you are trying to update the state by redirect while the prosses of rendering NewUser this create an infinite loop
usuing useEffect here will help you perform update after the component render
In the
NewUser
component you are attempting to navigate as an unintentional side-effect from directly in the function component body. This is the cause of the "Cannot update a component(RouterProvider
) while rendering a different component(NewUser
)." warning.To resolve place the side-effect logic into a
useEffect
hook call with appropriate dependencies such that it is now an intentional side-effect.Example:
You can also return/throw a redirect directly from route loaders and actions. If using this then remove the
useNavigate
hook call and redirect logic from theNewUser
component since it’s all handled in the action now.Example: