useffect inside <outlet> component does not trigger - reactjs

Parent component:
<Main props... >
<LinksArray />
<Outlet context={investorId}/>
</Main>
outlet component
const NewBoards: React.FC = () => {
let { boardId } = useParams();
// this does not
useEffect(() => {
console.log('ue trigger page change',boardId )
}, []);
// this triggers (because of an argument
useEffect(() => {
console.log('ue trigger page change',boardId )
}, [boardId ]);
return (
<>
{console.log('>rerender')}
<p> newBoards</p>
<p>{boardId}</p>
</>
)
}
NewBoards is an outlet element, I would love for useEffect to trigger on a ROUTE (ex. boards/123 to boards/345 ) change, without passing boardId, however useEffect does not trigger on the address change unless I`m passing boardId to the dependency array. boardId param does change.
Also I would like to know why that useEffect does not trigger. I can't find anything related to it on the react router v6 official documentation
edit:
I have also noticed that states are saved. States inside outlet component( NewBoards ) do not refresh to initial ones.
edit ( router definition ) :
{
path: '/boards/',
element: authorized ? <Boards1 /> : <Login />,
children: [{
path: '/boards/:boardId',
element: authorized ? <NewBoards /> : <Login />,
}]
},

From what I see from your comments you've seriously misunderstood what is happening between the Outlet and the nested Route components that are rendering their content, i.e. element prop, into it.
Assuming authorized is true then the following route config:
{
path: '/boards/',
element: authorized ? <Boards1 /> : <Login />,
children: [{
path: '/boards/:boardId',
element: authorized ? <NewBoards /> : <Login />,
}]
},
will produce the following rendered routes:
<Routes>
...
<Route path="/boards" element={<Boards1 />}>
<Route path="/boards/:boardId" element={<NewBoards />} />
</Route>
...
</Routes>
Where I think your understanding goes awry is in thinking that when the URL path changes from "/boards/123" to "/boards/345" that the Boards1 component rendered on "/boards" will rerender and remount the Outlet. It won't. This means that the Outlet it is rendering also doesn't do anything other than output the result of the currently matched route.
A second point of confusion/misudnerstanding on your part is thinking that when the URL path changes from "/boards/123" to "/boards/345" that <Route path="/boards/:boardId" element={<NewBoards />} /> will unmount and remount a new instance of the NewBoards component. This is also not the case. The NewBoards component will remain mounted and will simply rerender. This is an optimization by react-router-dom as it's a lot more work to tear down and remount a component than it is to simply rerender it with some different props/context/etc.
The routed components using the route path params necessarily need to "listen" to changes to the params if they need to issue side-effects based on the param values. This is why the useEffect hook with empty dependency array runs only once when the NewBoards component was mounted (not each time the route changes) and the useEffect hook with the boardId param as a dependency correctly reruns each time the boardId value changes.
const NewBoards: React.FC = () => {
const { boardId } = useParams();
// Run this when component mounts, occurs once per mounting
useEffect(() => {
console.log('ue trigger page change', boardId);
}, []);
// Run this when the `boardId` param updates
useEffect(() => {
console.log('ue trigger page change', boardId);
}, [boardId]);
// Run this each render, i.e. no dependency array at all!
useEffect(() => {
console.log('>rerender');
});
return (
<>
<p>newBoards</p>
<p>{boardId}</p>
</>
);
};

Related

React - update or re-render parent component when navigating to another child

I have two components ChildA and ChildB which share a fair amount of UI elements plus the same logic to retrieve data from API before the component is rendered. The common logic and UI elements are externalized in Parent component.
The API call is executed from Parent in useEffect hook.
The common UI elements are rendered based on the data retrieved by the API call.
Problem:
When I navigate from /parent/a to /parent/b, the API call is not executed so the component doesn't update.
I guess the reason is Parent has already been mounted the first time I entered {base_url}/parent/a in the browser, so it doesn't execute useEffect a second time when I try to navigate to /parent/b. However I have no idea how to solve it. Am I going all wrong here?
Steps to reproduce:
Enter URL {base_url}/parent/a in the browser ; the API call is executed, everything works fine
Click a link to navigate to /parent/b ; the component doesn't update.
Requirements:
I would like to avoid using class components as much as possible
I don't use redux and I don't want to use it just to solve this problem
Routing:
<Route path="/parent/a" render={props => <Parent child={ChildA} name="child-a" {...props} /> } exact />
<Route path="/parent/b" render={props => <Parent child={ChildB} name="child-b" {...props} /> } exact />
Child A :
export default function ChildA() {
return (
<div>
<h1>Child A</h1>
{/*Clicking this link will reproduce the issue*/}
<Link to="b">Go to Child B</Link>
</div>
)
}
Child B :
export default function ChildB() {
return (
<div>
<h1>Child B</h1>
{/*Clicking this link will reproduce the issue*/}
<Link to="a">Go to Child A</Link>
</div>
)
}
Parent :
interface ParentProps {
child: any,
name: string
}
export default function Parent(props: ParentProps) {
const Child = props.child
const [model, setModel] = useState<Model | null>(null)
useEffect(() => {
fetchData()
}, [])
function fetchData() {
console.log('EXECUTING API CALL FOR:', props.name)
// ...API call ommitted for brevity...
}
return (
<div>
<h1>Parent</h1>
{/* ...Display lots of UI depending on Model - omitted for brevity... */}
<Child/>
</div>
)
}
Since you're passing an empty dependencies array, the effect only gets run once. You can add props.name to the dependencies array to make sure fetchData gets called when props.name changes.
useEffect(() => {
function fetchData() {
console.log('Executing API call for:', props.name)
// Fetch data
}
fetchData()
}, [props.name])
The fetchData function is added inside the effect to make the effect's dependencies clearly visible. I would recommend reading this for more information. And you can use the exhaustive-deps ESLint rule which is a part of the eslint-plugin-react-hooks package to help you find effects where the dependencies array does not include all the effect's dependencies.

How do I stop nested React components that dispatch Redux actions which update state from getting stuck in an infinite loop?

I am having an issue with my nested react components getting stuck in an infinite loop.
The outer component is a DashboardLayout. To separate redux logic from 'pure' layout logic I have divided the compnent as follows
DashboardLayout/
index.js
DashboardLayout.js
The dashboard layout is mapped to the route /user
The index.js is (roughly) as follows
const Dashboard = () => {
const { submissions } = useSubmissionsPreloader()
const dispatch = useDispatch()
const { pathname } = useLocation()
useEffect(() => {
if (pathname === '/dashboard') dispatch(replace('/dashboard/tasks'))
}, [dispatch, pathname])
return pathname === '/user' ? null : (
<DashboardLayout submissions={submissions} selected={pathname} />
)
}
DashboardLayout.js is roughly as follows
const DashboardLayout = ({
submissions,
selected
}) => (
<Container>
<SegmentedController
tabs={[
{ path: '/dashboard/submissions', title: 'My Submissions' },
{ path: '/dashboard/tasks', title: 'My Tasks' }
]}
selected={selected}
/>
<Switch>
{dashboardRoutes.map(({ path, loader, exact }) => (
<Route key={path} path={path} component={loadable({ loader })} exact={Boolean(exact)} />
))}
</Switch>
<h4>Submissions ({submissions.length})</h4>
<Table
headers={submissionsHeaders}
rows={submissions.map(submissionsToRows)}
/>
</Container>
)
This all works fine if the sub-component being mounted doesn't affect the redux state. However if we take one of the sub-components as an example
Tasks/
index.js
Tasks.js
index.js is as follows
const Tasks = () => {
const { tasks } = useTasks()
return <PureTasks tasks={tasks} />
}
and Tasks.js is simply this (doesn't actually care about the tasks yet)
const Tasks = () => (
<>
<p>Tasks assigned to me go here</p>
</>
)
The problem is that the useTasks is using a useEffect hook to dispatch a loadTasks action, a saga picks it up, makes an API call, and then dispatches loadTasksSuccess with the loaded tasks. The reducer for that updates the tasks state with the tasks pulled from the api
useTasks
export const useTasks = () => {
const tasks = useSelector(getTasks)
const dispatch = useDispatch()
const doTasksLoad = useCallback(() => dispatch(tasksLoad()), [dispatch])
useEffect(() => {
doTasksLoad()
}, [doTasksLoad])
return { tasks }
and the relevant bit of the saga
function* worker({ type }) {
switch (type) {
case 'TASKS_LOAD':
try {
const tasks = yield call(loadTasks) // api call returns tasks
yield put(tasksLoadSuccess(tasks))
} catch (err) {
yield put(tasksLoadFail(err))
}
/* istanbul ignore next */ break
default:
break
}
}
Nothing controversial there.
The issue is that the change to the state causes the layout to re-render which causes the nested component to re-render which triggers the tasksLoad action again which triggers the tasksLoadSuccess action, which changes the state (tasksLoad sets isLoading to true and tasksLoadSuccess sets it to false again) and this causes an infinite loop.
I've got a gut feeling I ought to be using something like useMemo or useRef to somehow stop the constant re-rendering, but so far I'm not quite getting that to work either.
This general mechanism is fairly core to the way I was planning on building the app so I'd like to get it right. If the nested component only reads from the state and doesn't change it then no re-rendering happens so a brute force approach would be to get the dashboard to simply preload everything it thinks it might need. But that seems ugly to me.
Has anyone got any suggestions as to a better way to approach this?
I have very less experience with Redux but i think const doTaskLoad is being assigned a function. If that is so then...
I think the problem over here is that you are using a function as a element in the dependency array of useEffect , as per the rules of React Render , every time a render happens every function is a new reference , hence React considers it as a new element and keeps on re rendering the value.
May i suggest using a primtive value for the dependency array
export const useTasks = () => {
const tasks = useSelector(getTasks)
const dispatch = useDispatch()
const doTasksLoad = useCallback(() => dispatch(tasksLoad()), [dispatch])
useEffect(() => {
doTasksLoad()
}, [doTasksLoad])
return { tasks }
I worked out what was wrong, with thanks to #cory-harper whose comment above pointed me in the right direction.
The Tasks component was indeed being reloaded. This is due to how my dashboardRoutes were being described.
I render the individual routes as
{dashboardRoutes.map(({ path, loader, exact }) => (
<Route key={path} path={path} component={loadable({ loader })} exact={Boolean(exact)} />
))}
which is all well and good, and standard practice, when rendering entire scenes. The loadable component wrapper ensures that the component is lazily loaded, and the route itself was being defined via a utility function makeRoute which is as follows
const makeRoute = ([path, scene, permissionRequired = null, exact = true]) => ({
path,
loader: () => import(`scenes/${scene}`),
permissionRequired,
exact
})
So the loader asynchronously imports the correct component which is fine for an entire scene, but for the nested component it made react think the component was new each time, thus the useEffect was being called each time, thus the infinite loop.
By removing the fancy-pants lazy/asynchronous loading (not needed as these sub-components are tiny) and replacing that loop with
<Switch>
<Route path="/dashboard/submissions" component={Submissions} exact />
<Route path="/dashboard/tasks" component={Tasks} exact />
</Switch>
the components no longer look unique each load, and everything works as expected.

How to prevent infinite loop with React's useReducer, useContext and useEffect

I'm currently trying to figure out how to avoid creating an infinite loop when wrapping my application in a Context provider (taking in values from useReducer) and then updating via child component with a useEffect hook.
There's an example of the problem here on CodeSandbox.
Obviously it's difficult to talk about the problem without reposting all the code here, but key points:
Root:
function App() {
const [state, dispatch] = useReducer(reducer, initialState);
const value = { state, dispatch };
return (
<Context.Provider value={value}>
...
</Context.Provider>
Child:
export const Page1: FC = () => {
const { dispatch, state } = useContext(Context);
const { isLoading } = state;
useEffect(() => {
dispatch({
type: "loading",
payload: false
});
}, [dispatch]);
return (...)
I'm probably missing something obvious, but any pointers might help others who run into the same problem.
Full example on CodeSandbox.
The root of the problem is here
<Route path="/page1" component={() => <Page1 />} />
When you pass inlined arrow function as component you basically creating new component for every render and forcing Route to completely re-mount this part. When it happens useEffect gets called again and so on, and so on.
You need to change it like that:
<Route path="/page1"><Page1 /></Route>
// or
<Route path="/page1" component={Page1} />
Citation from react-router docs:
When you use component (instead of render or children, below) the router uses React.createElement to create a new React element from the given component. That means if you provide an inline function to the component prop, you would create a new component every render. This results in the existing component unmounting and the new component mounting instead of just updating the existing component. When using an inline function for inline rendering, use the render or the children prop (below).
Source: https://reactrouter.com/web/api/Route/route-render-methods

pass values to parent element react hooks

Hope you can clarify this issue:
I have parent component:
function App() {
const [ thanks, setThanks ] = useState(null);
const thankerHandler=(value)=>{
setThanks(value)
}
**//in the return**
<Route component={PaymentPage} thankerHandler={thankerHandler} path="/payment" />```
and the child component :
const PaymentPage=({thankerHandler})=> {
const [thanks, setThanks] = useState(false);
**//after some logic**
useEffect(() => {
thankerHandler(thanks);
}, [thanks])
the problem is that react is telling me the following:
61 | useEffect(() => {
62 |
> 63 | thankerHandler(thanks);
64 |
65 | }, [thanks])
thankerHandler is not a function
I have no idea what I am doing wrong here, the idea is that when the function will be called in the child component, it will update the value in the parent, but it is not doing the thing, when I console log thankerHandler, just gives me the value of undefined because of the null state in the parent, if I set it there to false, gives me false and so on, but it does not recongnices it as a function.
anyone knows what I am doing wrong here?
thanks in advance
Since PaymentPage component is rendered via Route component, props passed to Route component are not passed to the PaymentPage component.
Instead of using component prop to render the PaymentPage component, use render prop which takes a function. It will allow you to pass the props to PaymentPage component
Change
<Route component={PaymentPage} thankerHandler={thankerHandler} path="/payment" />
to
<Route
path="/payment"
render={(routerProps) => <PaymentPage {...routerProps} thankerHandler={thankerHandler} />}
/>
Apart from this problem, there is another problem in PaymentPage component.
Since useEffect is calling the thankerHandler function, thankerHandler should be added to the dependency array of the useEffect hook
useEffect(() => {
thankerHandler(thanks);
}, [thanks, thankerHandler]);
but adding thankerHandler to the dependency array of useEffect hook will cause another problem, i.e. when App component re-renders, thankerHandler function will be re-created and the reference to this new function will be passed to PaymentPage component which will cause the useEffect hook in PaymentPage component to run again. This may or may not be something you want. If you don't want this, make sure to wrap thankerHanlder function in useCallback hook to prevent useEffect hook from running unnecessarily.
try to use render method in Route component:
<Route render={() => <PaymentPage thankerHandler={thankerHandler} />} path="/payment" />

React warning about setState in unmounted component

I'm getting this error:
warning.js:33 Warning: Can't call setState (or forceUpdate) on an
unmounted component. This is a no-op, but it indicates a memory leak
in your application. To fix, cancel all subscriptions and asynchronous
tasks in the componentWillUnmount method.
But I'm not using a componentWillUnMount method.
I'm using a HOC to make sure the user is authenticated before accessing their /account route.
Here's the Route:
<StyleRoute props={this.props} path="/account" component=
{RequireAuth(Account)} />
where RequireAuth is the HOC. Here's the HOC:
import { withRouter } from 'react-router';
export default function RequireAuth(Component) {
return class AuthenticatedComponent extends React.Component {
componentWillMount() {
this.checkAuth();
}
checkAuth() {
if ( ! this.props.isAuthenticated) {
this.props.history.push(`/`);
}
}
render() {
return this.props.isAuthenticated
? <Component { ...this.props } />
: null;
}
}
return withRouter(AuthenticatedComponent);
}
The code works as intended, but I'm getting that error when /account is rendered. As you notice, nowhere in my direct code is there an componentWillUnMount method. I'm really at a loss for why this warning keeps popping up and any info would help.
Update 5/23/18:
To get rid of the error and still have props pass down, I did two thing:
1) I opted for a having two higher order functions in parent App component instead of using the HOC. One higher order function is for passing props and the other is to check authentication. I was having trouble passing any props other than the browser history, hence the renderProps function below.
renderProps = (Component, props) => {
return (
<Component {...props} />
);
}
checkAuth = (Component, props) => {
if (props.isAuthenticated) {
return <Component {...props} />
}
if (!props.isAuthenticated) {
return <Redirect to='/' />
}
}
2) To use these, I had to user render in my Route, as opposed to component.
//I could pass props doing this, sending them through the above functions
<Route exact path="/sitter-dashboard" render={ () => this.checkAuth(SitterDashboard, this.props) } />
<Route exact path={"/account/user"} render={() => this.renderProps(User, this.props)} />
//I couldn't pass props doing this
<Route {...this.props} exact path="/messages" component={Messages} />
Here's the documentation on router vs component as a Route render method: https://reacttraining.com/react-router/web/api/Route/route-render-methods
Also, here's a good explanation on Stack Overflow
Finally, I used this code from the React Router 4 documentation as a template for what I did above. I'm sure the below is cleaner, but I'm still learning and what I did makes a bit more sense to me.
const PrivateRoute = ({ component: Component, ...rest }) => (
<Route
{...rest}
render={props =>
fakeAuth.isAuthenticated ? (
<Component {...props} />
) : (
<Redirect
to={{
pathname: "/login",
state: { from: props.location }
}}
/>
)
}
/>
);
I had the same error time ago and it was generated by a component which was using ref tag, and there was some manual manipulation.
A good practice to see these kind of errors is drawing your app flow and see when your are calling setState.
Another thing I would change if I were you is componentDidMount instead of componentWillMount to check some data. Take into account fb deprecated this functionality.
This lifecycle was previously named componentWillMount. That name will continue to work until version 17. Use the rename-unsafe-lifecycles codemod to automatically update your components.
Reactjs component documentation
I had a similar problem, but I did figure out the reason behind the same, so here is the snippet of code where I was encountering this err.
Warning: Can't call setState (or forceUpdate) on an unmounted component. This is a no-op, but it indicates a memory leak in your application. To fix, cancel all subscriptions and asynchronous tasks in the componentWillUnmount method.
Cause:
this.setState({ showLoader: true });
const { username } = this.state;
const URL = `https://api.github.com/users/${username}`;
try {
const { data } = await axios(URL);
this.props.apiData(data);
this.props.history.push("profile");
} catch (e) {
console.error(e);
}
this.setState({ showLoader: false });
As you can see in the code-snippet, I was doing
this.props.history.push("profile");
before setting the state.
this.setState({ showLoader: false });
And then err seems to be legit in this case as I was redirecting to a different component and then setting the state on the component I was earlier.
Solution:
By placing
this.setState({ showLoader: false });
above the this.props.history.push("profile"); solved the problem.
I hope this helps.

Resources