I use the useEffect hook to dispatch the getQuestions function in order to get the data from the server
function App () {
const dispatch = useDispatch();
useEffect(() => {
dispatch(getQuestions());
}, [dispatch]);
return (
<Routes>
<Route exact path="/" element={<Layout/>}>
<Route path="repetition" element={<Repetition/>}/>
<Route path="family" element={<Family/>}/>
</Route>
</Routes>
);
}
The problem is that when I, for example, open the family link (which I declared in the App function), initially I get the data, but when I refresh the page, the data disappears.
I certainly understand that when the page is refreshed the parent App component is not rendered from this and I get an error, similar issues I have looked at in the forums where it was suggested to use withRouter which updates the parent component, but my version of react-router-dom does not supports withRouter, except that I don't want to downgrade my version of react-router-dom to use withRouter.
I would like to know if there is any way to fix this problem.
I tried the option that #Fallen suggested, i.e. I applied the useEffect hook in each child element and analyzed this approach in GoogleLighthouse, and I'm happy with the results.
Here is my final code in child component
function Family () {
const dispatch = useDispatch();
const questions = useSelector(state => state.QuestionsSlices.familyQuestions);
useEffect(() => {
dispatch(getFamilyQuestions());
}, [dispatch]);
return (
<>
{questions.data.map((item, idx) => (
<div key={idx}>
{ idx + 1 === questions.score && CheckQuestionsType(item, questions) }
</div>
))}
</>
);
}
Related
I came back to react world after a few years. And things certainly have changed for good. I'm using MemoryRouter for my app. And I can navigate fine by using Link. But useNaviate hook is not working as expected. It does nothing on the page. Could you please help me here? Here is my code:
Router:
<MemoryRouter>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/dashboard" element={<Dashboard />} />
</Routes>
</MemoryRouter>
Here is how I'm trying the navigation:
function Home() {
// demo purpose
const navigate = useNavigate()
navigate('/dashboard')
}
I'm not sure if I'm using it right, or if I need to do something else here.
The code is calling navigate as an unintentional side-effect directly in the function component body.
Either call navigate from a component lifecycle or callback to issue an imperative navigation action:
function Home() {
const navigate = useNavigate()
useEffect(() => {
if (/* some condition */) {
navigate('/dashboard');
}
}, [/* dependencies? /*]);
...
}
Or conditionally render the Navigate component to a declarative navigation action:
function Home() {
...
if (/* some condition */) {
return <Navigate to="/dashboard" />;
};
...
}
The problem was that I was calling navigate directly when the component was rendering. It should either be called in an event, or it should be called in useEffect hook.
Make your navigate in function call or in useEffect like this:
function Home() {
// demo purpose
const navigate = useNavigate()
useEffect(() => {
navigate('/dashboard')
}, []);
}
I am building a Simple React App and I am using api to set state and I am using Link to get into detail page But When I go into another page from Link and press back button then It is setting state again. But I am trying to prevent setting state again when I go back to the page from back button.
App.js
function MainPage() {
const [blogs, setBlogs] = useStae([]);
useEffect(() => {
axios.get("/api/blogs/").then((res) => {
setBlogs(res.data.blogs);
}
}
return (
<>
{
blogs.map((res) => <>
<b>{res.title}
// It is showing about 40 blogs.
<Link to={`blog-detail/${res.id}`}>Blog Detail</Link>4
</>
}
</>
)
}
When I click on Blog Detail and press button in Browser (Chrome) then It is setting state again.
What I have tried ?
I have also thought about Using LocalStorage with Redux Store like :-
const saveToLocalStorage = (state) => {
try {
localStorage.setItem('state', JSON.stringify(state));
} catch (e) {
console.error(e);
}
};
But the Array was 40 in length then I thought Would it be efficient to store that big array (40 maybe 100(max)) in localStorage ?`
Then I cancelled it.
Using only localStorage without redux but It was localStorage after all.
I have tried many times but it is still setting state with makeing request to backend server.
Any help would be much Appreicated.
If you want to keep the MainPage component from running the effect each time it mounts then you'll need to Lift State Up to a common parent/ancestor component that remains mounted while the app's router is matching and rendering different routed content/pages/components.
You can move the blogs state up to the component rendering the routes so the data is fetched only once when mounted and lives for the duration of the component's life. Don't forget to include a dependency array for the useEffect hook!
Example:
const App = () => {
const [blogs, setBlogs] = useState([]);
useEffect(() => {
axios.get("/api/blogs/")
.then((res) => {
setBlogs(res.data.blogs);
});
}, []); // <-- empty dependency array, run effect once on mount
return (
<Routes>
<Route
path="/blog"
element={<MainPage blogs={blogs} />} // <-- pass blogs as prop
/>
<Route
path="/blog-detail/:id"
element={<BlogDetails blogs={blogs} />} // <-- pass blogs as prop
/>
</Routes>
);
};
...
function MainPage({ blogs }) { // <-- consume blogs as prop
return (
<>
{blogs.map((blog) => (
<React.Fragment key={blog.id}>
<b>{blog.title}</b>
// It is showing about 40 blogs.
<Link to={`blog-detail/${blog.id}`}>Blog Detail</Link>
</React.Fragment>
)}
</>
)
}
Switching/navigating between routes now will no longer trigger the useEffect hook to refetch the blogs data.
I'm trying to build a dynamic router with React. The idea is that the Routes are created from the data (object) received from the backend:
Menu Object:
items: [
{
name: "dashboard",
icon: "dashboard",
placeholder: "Dashboard",
path: "/",
page: "Dashboard",
exact: true,
},
{
name: "suppliers",
icon: "suppliers",
placeholder: "Suppliers",
path: "/suppliers",
page: "Suppliers",
exact: true,
}
]
The routes hook :
export const RouteHook = () => {
// Dispatch to fetch the menu object from the backend
const dispatch = useDispatch();
dispatch(setMenu());
// Select the menu and items from Redux
const { items } = useSelector(getMainMenu);
// useState to set the routes inside
const [menuItems, setMenuItems] = useState([]);
const { pathname } = useLocation();
useEffect(() => {
// Loop trough the menu Items
for (const item of items) {
const { page, path, name, exact } = item;
// Import dynamically the route components
import(`../pages/${name}/${page}`).then((result) => {
// Set the routes inside the useState
setMenuItems(
<Route exact={exact} path={path} component={result[page]} />
);
});
}
// Check if pathname has changed to update the useEffect
}, [pathname]);
return (
<Switch>
{/* Set the routes inside the switch */}
{menuItems}
</Switch>
);
};
Now here's the problem. Not all the components load. Usually the last component loads and when clicking on a diffrent route the component won't change. Except if you got to the page and refresh (F5).
What am I missing here? Is it possible to create full dynamic routes & components in react?
I'm not sure 100% what's going on here, but here's a problem I see:
const [menuItems, setMenuItems] = useState([]);
You're saying that menuItems is an array of something. But then:
import(`../pages/${name}/${page}`).then((result) => {
// Set the routes inside the useState
setMenuItems(
<Route exact={exact} path={path} component={result[page]} />
);
});
On every iteration you are setting the menu items to be a singular Route component. Probably what you're thinking youre doing is
const routes = items.map(item => {
const { page, path, name, exact } = item;
return import(`../pages/${name}/${page}`).then((result) => {
<Route exact={exact} path={path} component={result[page]} />
});
})
setMenuItems(routes)
But this makes no sense, because your map statement is returning a Promise.then function. I'm not entirely sure why you're dynamically importing the components here. You're better off doing a simple route mapping:
const routes = items.map(item => {
const { page, path, name, exact } = item;
return <Route exact={exact} path={path} component={components[page]} />
})
setMenuItems(routes)
Where components is an object whose keys are the values of page and whose values are actual components, i.e.:
const components = {
Suppliers: RenderSuppliers,
Dashboard: RenderDashboard
}
If you want these components lazy-loaded, use react suspense:
const Suppliers = React.lazy(() => import("./Suppliers"))
const Dashboard = React.lazy(() => import("./Dashboard"))
const components = {
Suppliers,
Dashboard,
}
const routes = items.map(item => {
const { page, path, name, exact } = item;
return (
<Suspense fallback={<SomeFallbackComponent />}>
<Route
exact={exact}
path={path}
component={components[page]}
/>
</Suspense>
)
})
setMenuItems(routes)
This is just a quick review of what may be going wrong with your code, without a reproducible example, its hard to say exactly.
Seth has some great suggestions, but here's how you can clean this up while still using dynamic imports.
Hopefully you can see that you are calling setMenuItems with a single Route component instead of all of them. Each time that you setMenuItems you are overriding the previous result and that's why only the last Route actually works -- it's the only one that exists!
Your useEffect depends on the pathname which seems like you are trying to do the routing yourself. Since you are using react-router-dom you would include all of the Route components in your Switch and let the router handle the routing.
So you don't actually need any state here.
You can use the React.lazy component import helper inside of the Route. You need a Suspense provider around the whole block in order to use lazy imports.
I don't like that you use two variables in the path for a component ../pages/${name}/${page}. Why not export the component from the ./index.js of the folder?
export const Routes = () => {
// Dispatch to fetch the menu object from the backend
const dispatch = useDispatch();
dispatch(setMenu());
// Select the menu and items from Redux
const items = useSelector((state) => state.routes.items);
return (
<Suspense fallback={() => <div>Loading...</div>}>
<Switch>
{items.map(({ exact, path, name }) => (
<Route
key={name}
exact={exact}
path={path}
component={React.lazy(() => import(`../pages/${name}`))}
/>
))}
</Switch>
</Suspense>
);
};
It works!
Code Sandbox Link
I am trying to add Google Analytics to React using the guide found here.
I have
import React, { useEffect } from "react";
import ReactGA from "react-ga";
ReactGA.initialize("UA-0000000-0");
export default (WrappedComponent, options = {}) => {
const trackPage = page => {
ReactGA.set({
page,
...options
});
ReactGA.pageview(page);
};
const HOC = props => {
useEffect(() => trackPage(props.location.pathname), [
props.location.pathname
]);
return <WrappedComponent {...props} />;
};
return HOC;
};
Then, I call it like this:
<BrowserRouter className={classes.root}>
<Switch>
<Route path="/login" component={withTracker(Login)} />
<Route path="/signup" component={withTracker(SignUp)} />
</Switch>
</BrowserRouter>
The full error:
react_devtools_backend.js:2273 Warning: Can't perform a React state update 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 a useEffect cleanup function.
in SignIn (at GoogleAnalytics.js:64)
in HOC (created by Context.Consumer)
I have tried with the react hooks method they show as well as the other suggested way on that page but still get the same error. I am new to React so struggling to find the issue.
Thanks!
Trying to set the state in ReactGA.set while component is unmounted can give that warning. You can set a variable 'Mounted' to true in useEffect and in the return function set it to false which is executed when component unmounts.
const HOC = props => {
useEffect(() => {
let isMounted = true
isMounted &&
trackPage(props.location.pathname)
return () => isMounted = false
}, [props.location.pathname]);
If you still get the warning move the ReactGA.set inside useEffect and use Mounted && ReactGA.set.....
This is a tricky problem.
A bit of info: I can have multiple collections of multiple images and a specific header logo for each collection. My collection url looks like this /collection/{collectionId}/item/{itemId}
const MyApp = () => {
// unrelated code
return(
<BrowserRouter>
<SiteLayout>
</BrowserRouter>
)
}
and
const SiteLayout = () => {
location = useLocation()
collectionId = location.pathname.split('/')[2] //gives me the collection Alias
const [collData, setCollData] = useState()
useEffect(() => {
const getCollectionData = async() => {
//fetch collection data
//setCollData(fetched data)
}
getCollectionData()
}, [collectionId])
return(
<CollectionContext.Provider value={collData}>
<div className='mainContainer'>
<div className='headerContainer'>
<Header/>
</div>
<div className='mainContent'>
<Route exact path='/collection/:collectionId' component={CollectionViewPage}>
<Route exact path='/collection/:collectionId/item/:itemId' component={ItemViewPage}
</div>
<div className='footerContainer'>
<Footer/>
</div>
</div>
</CollectionContext.Provider>
)
}
export default SiteLayout
My thoughts:
I set it up this way, because I cant use useLocation() outside of <BrowserRouter> and I want to update my collectionContext (which contains information about a the collection I'm on, including the collection specific header logo) when the collectionId changes, which is part of the location
My issue:
When I move from item to item, within a collection, the header and everything else still re-renders, for example
from -- /collection/collection1/item/1
to -- /collection/collection1/item/2
My thoughts: This header shouldn't update because the collectionId never changed. However, when I look at the Profiler in React dev tools, it says the Header changed because the parent changed, following it up to <Route> which changed because it's state (location) changed.
What I'm looking for: How do I refactor this to update when the collectionId changes, but not every time the location changes? I need this because (many things, but for simplicity) I want the header to update when I change collections, so that it can use the correct logo, but I would like to prevent the <Header> from re-rendering if I'm navigating around within the collection.
Something else I've tried:I've tried ripping out useLocation and instead using window.location.pathname but nothing will update.
Solved
Using the suggestion from #HMR in response to the original question:
If Header and CollectionViewPage are functional components maybe you can wrap [them] in React.memo If they still re render [...] look at what you need and maybe create a container that only picks what you actually need...
Implementation:
By adding a middle layer, I am able to split location, and then memoize SiteLayout based on whatever part of it I would like.
const MyApp = () => {
// unrelated code
return(
<BrowserRouter>
<LocationPartition>
</BrowserRouter>
)
}
export default MyApp
where in LocationPartition (could probably use a better name):
const LocationPartition = () => {
const location = useLocation()
const collectionId = location.pathname.split('/')[2]
return(
<SiteLayout collection={collectionId}/>
)
}
export default LocationPartition
and so in SiteLayout, I can now memoize based off the collection prop passed from LocationPartition
const SiteLayout = memo(({collection})) => {
const [collData, setCollData] = useState()
useEffect(() => {
const getCollectionData = async() => {
//fetch collection data
//setCollData(fetched data)
}
getCollectionData()
}, [collection])
return(
<CollectionContext.Provider value={collData}>
<div className='mainContainer'>
<div className='headerContainer'>
<Header/>
</div>
<div className='mainContent'>
<Route exact path='/collection/:collectionId' component={CollectionViewPage}>
<Route exact path='/collection/:collectionId/item/:itemId' component={ItemViewPage}
</div>
<div className='footerContainer'>
<Footer/>
</div>
</div>
</CollectionContext.Provider>
)
}
export default SiteLayout
And lo and behold ... React Profiler now only shows re-renders where I want them!