running into an issue with ISR and SWR on a dynamic page when updating content.
I have a page under product/[slug], that uses staticProps and Paths to generate the pages/routes
export const getStaticProps: GetStaticProps = async ({ params }) => {
const slug = params!.slug;
const products = await request(baseUrl, productsBySlug, {
slug: slug,
});
return {
props: {
products: products,
},
revalidate: 10,
};
};
export const getStaticPaths: GetStaticPaths = async () => {
const products = await request(baseUrl, allProductsQuery);
const paths = products.products.map((product: Product) => ({
params: { slug: product.slug },
}));
return { paths, fallback: true };
};
now, when I change the title of a product inside of the graphql-playground the slug is automatically updated (this is in laravel).
The problem here is that, the url won't change to reflect the potential slug update e.g.
my current url is product/test-product.
someone updates the name of the product to Updated product which is transformed to a slug which means the url is now product/updated-product.
But because the paths are statically generated next cannot find this and it gives an error when fetching.
I'm also using SWR to fetch data periodically and to keep it synced but this also seems to not work (unless i'm doing something wrong).
again on the product/[slug] page I have SWR running to update the data in real time for the current user, while using ISR to update it for any subsequent requests
const variables = {
slug: router.query.slug,
};
const { data, error } = useSWR([productsBySlug, variables], {
fallbackData: products,
revalidateOnMount: false,
});
if (router.isFallback) {
return (
<Box
sx={{
mx: "auto",
}}
>
<Skeleton animation="wave" variant="circular" width={40} height={40} />
</Box>
);
}
if (!error && data) {
return (
<Box
sx={{
mx: "auto",
}}
>
<Head>
<title>Cross-poc | {data.productBySlug.slug}</title>
</Head>
<Button variant="outlined">
<Link
href={`edit/${encodeURIComponent(
data.productBySlug.id
)}/${encodeURIComponent(data.productBySlug.slug)}`}
>
Edit this product
</Link>
</Button>
<ProductComponent
showButton={false}
key={data.productBySlug.id}
product={data.productBySlug}
/>
</Box>
);
}
return <Alert severity="error">Could not fetch product info</Alert>;
If I'm on the product page when it's updated I alway get the Alert popup to display.
Is their anyway I can make URL's change dynamically with next as well?
Related
There is a React component which contains list of users and form to invite a new user.
"inviteNewUser" is a *POST* request in backend
"getUsers" is a *GET* request to get all users
The problem is that after clicking on button "Invite User" I would like to see the invited user in the list of users ("currentUsers" in code below) without refreshing the page. But right now it happens only after I refresh the whole page.
when I'm trying to make a GET request to get all users right after inviteNewUser(data) (POST request) I'm getting the "old" user list without user which I just invited. So the "currentUsers" list is not immediately updated
Could someone help me to fix this issue ?
export function MyForm({
getUsers,
inviteNewUser,
userId,
currentUsers
}) {
useEffect(() => {
getUsers(userId);
}, [userId]);
function handleSendInvite(data) {
inviteNewUser(data);
getUsers(data.userId);
}
return (
<>
{currentUsers.map((user) => (
<UserItem
key={user.userId}
user={user}
/>
))}
<Button
text="Invite User"
onClick={() => {
handleSendInvite({userId});
}}
/>
</>);
}
MyForm.propTypes = {
getUsers: PropTypes.func.isRequired,
inviteNewUser: PropTypes.func.isRequired,
userId: PropTypes.number.isRequired,
currentUsers: PropTypes.arrayOf(UserInfo),
};
const mapStateToProps = (state) => {
const { id } = routerParamsSelector(state);
const currentUsers = selectCurrentUsers(state);
return {
userId: parseInt(id, 10),
currentUsers,
};
};
const mapDispatchToProps = {
getUsers,
inviteNewUser
};
export default connect(mapStateToProps, mapDispatchToProps)(MyForm);
Try async and await, it works.
const handleSendInvite = async (data) {
await inviteNewUser(data);
getUsers(data.userId);
}
It should be user instead of userId as you access the property of the object in the function.
<Button text="Invite User" onClick={() => { handleSendInvite(user); }} />
------------------------------------------------------------------------
const handleSendInvite = async (data) {
await inviteNewUser(data);
getUsers(data.userId);
}
Building a site where users upload images/video, in the component that handles that, I have the images load into the page after being uploaded, so that the user can make sure they want to post them, and have the option to remove them if they like. I originally had this as a Class-based view, and everything worked as expected, but now, after changing to a functional component, after uploading images, React doesn't seem to notice the changes to imageLinks (even though the console shows that imageLinks is getting added to), until I update something else like the post title, then they all load in as expected. Once loaded in, if I click the delete button, React instantly updates and the photos/videos no longer shows up, as expected.
Any ideas as to why this is behaving so oddly?
The fact that the deleteMedia function works just fine is what really weirds me out.
I set up my variables like this
export default function NewPost(props) {
const [postCategories, setPostCategories] = useState([]);
const [postTitle, setPostTitle] = useState();
const [postDescription, setPostDescription] = useState();
const [postHashtags, setPostHashtags] = useState();
const [imageLinks, setImageLinks] = useState([]);
...}
In my component, I have this to handle uploading and deleting files.
const uploadMedia = async (file) => {
var csrftoken = getCookie("csrftoken");
var media_id = makeMediaID();
let formData = new FormData();
formData.append("content_id", contentID);
formData.append("creator_id", USER_ADDRESS);
formData.append("content_media", file);
formData.append("media_id", media_id);
const response = await fetch("/api/newpost/media/", {
method: "POST",
headers: {
"X-CSRFToken": csrftoken,
},
body: formData,
});
console.log(response);
const r_json = await response.json();
if (r_json.success) {
let tempLinks = imageLinks;
tempLinks.push({
img: r_json.image_url,
id: r_json.m_id,
type: r_json.m_type,
});
setImageLinks(tempLinks);
console.log(imageLinks);
} else {
console.log(r_json.message);
}
};
const deleteMedia = async (media) => {
var csrftoken = getCookie("csrftoken");
let formData = new FormData();
formData.append("media_id", media);
formData.append("creator_id", USER_ADDRESS);
const response = await fetch("/api/newpost/media/delete/", {
method: "DELETE",
headers: {
"X-CSRFToken": csrftoken,
},
body: formData,
});
console.log(response);
const r_json = await response.json();
if (r_json.success) {
let tempLinks = imageLinks.filter((item) => item.id !== media);
setImageLinks(tempLinks);
} else {
console.log("Media deletion error");
}
};
And in my render, I have this, which worked just fine when it was a class-based component.
{imageLinks.map((item) => (
<Grid item xs={9} align="center" key={item.img}>
<Card style={{ maxWidth: 550, margin: 15 }}>
<div
style={{
display: "flex",
alignItem: "center",
justifyContent: "center",
}}
>
<CardMedia
style={{
width: "100%",
maxHeight: "550px",
}}
component={item.type}
image={item.img}
controls
title={String(item.img)}
/>
</div>
<div align="center">
<CardActions>
<Button
endIcon={<DeleteForeverIcon />}
label="DELETE"
color="secondary"
variant="contained"
onClick={() => deleteMedia(item.id)}
>
REMOVE
</Button>
</CardActions>
</div>
</Card>
</Grid>
))}
Issue
The issue is a state mutation.
const [imageLinks, setImageLinks] = useState([]);
tempLinks is a reference to the imageLinks state. You are pushing directly into this tempLinks array and saving the same array reference back into state. The code is also incorrectly attempting to log the state immediately after enqueueing the state update. This doesn't work as React state updates are asynchronously processed.
if (r_json.success) {
let tempLinks = imageLinks; // <-- reference to state
tempLinks.push({ // <-- state mutation!
img: r_json.image_url,
id: r_json.m_id,
type: r_json.m_type,
});
setImageLinks(tempLinks); // <-- same reference back into state
console.log(imageLinks); // <-- only logs current state
}
The imageLinks state array reference never changes so React doesn't see this as state actually being updated.
Solution
Create and return a new array reference for React.
if (r_json.success) {
setImageLinks(imageLinks => [ // <-- new array reference
...imageLinks, // <-- shallow copy previous state
{ // <-- append new element object
img: r_json.image_url,
id: r_json.m_id,
type: r_json.m_type,
}
]);
}
Use a separate useEffect hook to console log the state updates.
useEffect(() => {
console.log(imageLinks);
}, [imageLinks]);
I am getting dynamic title and log from API and passing it to nextJS Head for og:title and og:image. but it is not working. Any idea how can I make it work.
Here is my code.
function PageHead(props: any) {
let location = "/";
if (process.browser) {
location = window.location.href;
}
console.log(props);
return (
<Head>
<meta property="og:title" content={props.title} />
<meta property="og:url" content={location} />
<meta property="og:image" content={props.logo} />
</Head>
);
}
const Home: NextPage = () => {
const [title, setTitle] = useState('');
const [logo, setLogo] = useState('');
useEffect(() => {
(async () => {
const reviewData = await (await fetch("https://api.")).json();
setTitle(reviewData.name);
setLogo(reviewData.logo);
})();
}, []);
return (
<React.Fragment>
<Box
sx={{
display: 'flex',
flexDirection: 'column',
minHeight: '100vh',
}}
>
<PageHead logo={logo} title={title}/>
</Box>
</React.Fragment>
)
}
export default Home
After I export static site or run as a dev. when I refresh the page, in console.log I get the following 3 logs one after another. This tells me that it does get the logo and title from api one by one and refresh console but for some reason does not populate it within HEAD for title and image tag.
Any idea how can I fix it.
You need to init your page header at build time for SEO purpose
If you fetch data after your app has been rendered on the client-side. It's not meaningful for SEO anymore, because Search Engine Bot can't read your header at that time.
So you need to render your Header component on the server at build time.
Next.js provided you some method to support you fetch data at build time for all cases (server-side rendering, static site generate, incremental site regenerate...)
Here is their official documentation: https://nextjs.org/docs/basic-features/data-fetching
Here is an example for static generate my Blog detail page:
export const getStaticProps = async ({ params }) => {
const response = await fetchAPI(params.slug); // Fetch your data
// Response 404 page if data is failed to fetch
if (!response.success) {
return { notFound: true };
}
const { title, description, images } = response;
return {
props: {
pageHeader: {
title: name,
metas: [
{
name: 'description',
content: description,
},
{ property: 'og:title', content: name },
{
property: 'og:image',
content: images[0] || null,
},
{
property: 'og:description',
content: description,
},
],
},
productData: response,
},
revalidate: 5 * 60, // re-generate each 5 minutes
};
};
And all data fetched above will be passed to your Page Component. You can use these data props to render your Page Header at build time. This Header will be included in your first HTML document. Then Search Engine Bot can read and handle that.
import Head from 'next/head';
const BlogDetailPage = (pageProps) => (
<>
<Head>
<title>{pageProps.pageHeader.title}</title>
{pageProps.pageHeader.metas.map((attributes, index) => (
<meta {...attributes} key={index} />
))}
</Head>
<YourPageComponent />
</>
);
I have a small learning project build with React, TS, & Apollo.
As backend I am using https://graphqlzero.almansi.me/
The result I am looking for is:
Fetch Posts by page, 5 per page
Re-use previously fetched data
Here is my container
export const GET_POSTS = gql`
query GetPosts($options: PageQueryOptions!) {
posts(options: $options) {
data {
id
title
body
}
}
}
`
const Posts = (): JSX.Element => {
const [page, setPage] = useState<number>(1)
const { data, fetchMore} = useQuery(GET_POSTS, {
variables: {
options: {
paginate: {
page: 1,
limit: 5,
},
},
},
nextFetchPolicy: "cache-first"
},
)
return (
<div>
<ListOfPosts {...{data,fetchMore,page,setPage }} />
</div>
)
}
and ListOfPosts
const ListOfPosts = ({ data,fetchMore, page, setPage }) => {
const getNextPage = (): void => {
fetchMore({
variables: {
options: {
paginate: {
page: page + 1,
limit: 5,
},
},
},
})
setPage((p: number) => p + 1)
}
const getPrevPage = (): void => {
setPage((p: number) => (p === 0 ? p : p - 1))
}
console.log(data)
return (
<div>
<p>current page{page}</p>
<button type="button" onClick={getPrevPage}>
Get Prev Page
</button>
<button type="button" onClick={getNextPage}>
Get Next Page
</button>
{data &&
data?.posts?.data?.map((post: any) => (
<div key={post.id}>
<h4>{post.title}</h4>
<p>{post.body}</p>
</div>
))}
</div>
)
So if I send a query with page === 1 I get posts from 1 to 5, if page === 2 - posts from 6 to 10 and so on.
The problem is that if for example I send request in next sequence
page === 1 (initially sent by useQuery)
page === 2 ( sending with fetchMore)
page === 3 ( sending with fetchMore)
page === 2 ( sending with fetchMore)
on last request Apollo performs network request, despite data for that that request is already in cache
So my questions actually are:
how to configure Apollo cache to return required data without re-fetching it from a server
How to "invalidate" that data and tell Apollo that I need refresh that particular portion of data?
I think it should be somehow configured in cache typePolicies but haven't found a way to make it work - despite data is in cache( I can track it with browser extension) it is not returned in {data}=useQuery :/
Here is how my cache config looks like.
export const cache = new InMemoryCache({
typePolicies: {
Query: {
fields: {
posts: {
merge(existing, incoming) {
return [ ...existing,...incoming ]
}
}
}
}
}
})
So there are two types of pagination. In the first pagination, the "pages" are somehow known to the user and the user navigates through these via the UI. This seems to be what you want to do:
I would propose to keep it simple: Change the state variables, pass them into useQuery and render the results. Apollo will run the query again with the new page value every time the variables change. If Apollo has seen the same variables before (you go back to the previous page), Apollo can return the result from cache.
export const GET_POSTS = gql`
query GetPosts($options: PageQueryOptions!) {
posts(options: $options) {
data {
id
title
body
}
}
}
`
const Posts = (): JSX.Element => {
const [page, setPage] = useState<number>(1)
const { data } = useQuery(GET_POSTS, {
variables: {
options: {
paginate: {
page,
limit: 5,
},
},
},
nextFetchPolicy: "cache-first"
})
return (
<div>
<ListOfPosts {...{data,page,setPage }} />
</div>
)
}
const ListOfPosts = ({ data, page, setPage }) => {
const getNextPage = (): void => setPage((p: number) => p + 1);
const getPrevPage = (): void =>
setPage((p: number) => (p === 0 ? p : p - 1))
return (
<div>
<p>current page{page}</p>
<button type="button" onClick={getPrevPage}>
Get Prev Page
</button>
<button type="button" onClick={getNextPage}>
Get Next Page
</button>
{data &&
data?.posts?.data?.map((post: any) => (
<div key={post.id}>
<h4>{post.title}</h4>
<p>{post.body}</p>
</div>
))}
</div>
)
}
Merging the cache is only relevant when you want to continuously show results after another. Things like "load more" or "infinite scrolling". This means you want to continue adding to your result list. You would add more and more posts to the view and the old pages don't disappear. This is what fetchMore is designed for. If you want to do that have a look at the docs and this part specifically. Your problem is that you are currently mixing both approaches, which probably leads to weird results.
I am using drawer navigation with react navigation v5, and i have a screen called profile, that takes in a route prop, that i pass user id to. Problem is, when I visit a profile, and then logout, and log back in, i get an error saying that route.params.id is not an object, undefined. In my profile.tsx I checked where i use the route params, and its as in the shown code below:
useEffect(() => {
if (!route) {
return navigation.navigate("Søg Brugere");
}
getUser();
}, [route]);
and getUser function should not be executed, however I include it for clarlity.
const getUser = async () => {
if (!route) return;
try {
console.log(route.params.id);
const id = route.params.id;
setRefreshing(true);
const { data } = await (await HttpClient()).get(
config.SERVER_URL + "/api/user/get-user-by-id/" + id
);
setRefreshing(false);
setProfile(data.user);
setMatch(data.match);
setInitiated(true);
if (socket && user) {
const notificationData = {
url: `/profile/${user._id}`,
type: "new-visit",
text: `Nyt besøg fra ${user.displayName}`,
user: data.user,
resourceId: user._id,
};
socket.emit("notification", notificationData);
}
} catch (e) {
navigation.navigate("Søg Brugere");
}
};
and also a snippet of my logout function, used in drawer navigator:
<View style={{ flex: 1, justifyContent: "flex-end" }}>
<Button
onPress={async () => {
props.navigation.replace("Søg Brugere");
props.navigation.closeDrawer();
await AsyncStorage.removeItem("token");
setUser(null);
}}
title="Log Ud"
color="#F44336"
/>
</View>
i solved this by adding at the top of my rfc:
const id = route?.params?.id