useState in next.js when navigating using Link (next/link) - javascript

I have a next.js page to display a post, like a blog, and I use Link to move from a post to another. To show the proper post I use another component, something like this (note that I cut a lot of code for semplicity)
const Index: NextPage<PageProps> = (props) => {
const router = useRouter();
let post = props.post;
return (
<>
<Post post={post}/>
<Link href={`/post/${encodeURIComponent(props.next.id)}`} passHref={true}>
<a>next post</a>
</Link>
<Link href={`/post/${encodeURIComponent(props.previous.id)}`} passHref={true}>
<a>previous post</a>
</Link>
</>
);
};
export default Index;
export const getServerSideProps = async (context) => {
const session = await getSession(context);
const pid = context.params.pid;
let postManager = new PostManager();
let post = await postManager.getPost(pid);
let siblings = null;
if (post) {
siblings = await postManager.getSiblings(post);
}
return {
props: {
post: JSON.parse(JSON.stringify(post)),
next: (siblings && siblings.next) ? JSON.parse(JSON.stringify(siblings.next)) : null,
previous: (siblings && siblings.previous) ? JSON.parse(JSON.stringify(siblings.previous)) : null,
session: session,
}
};
};
what it is strange is that in the Post component seems like useState is not executed after the first time I load the page:
const Post = props => {
const [title, setTitle] = useState(props.post.title);
console.log(props.post.title);
console.log(title);
here when I load the page "directly" the two values are the same (which is fine). Then when I click on Link (in the page component), the second console.log is showing the value from the content I loaded directly.
This doesn't happen if I take off Link and I leave <a>, but of course using Link is faster so I'd prefer to use it.
How can I "force" useState to set the correct value every time props has changed?

Because you are staying on the same page with shallow routing the component is not freshly re-rendered but the props are updated. This is intended react/next behaviour.
Add to your Post component the following:
useEffect(() => {
setTitle(props.post.title);
}, [props]);
This will update the state when the props are updated.
Let me know if you have any questions.

Related

Update Server Component after data has changed by Client Component in Next.js

I am still trying to wrap my head around this scenario. Can anyone please suggest what is the correct way to do this in Next.js 13?
I diplay a list of users in a Server Component, for example, like this (using MongoDB):
// UsersList.jsx
const UsersList = () => {
const users = await usersCollection.getUsers()
return (
<div>
{users.map(user) => <div>{user}</div>}
</div>
)
}
And on the same page, I have also defined client component for adding users:
// UsersEdit.jsx
'use client'
const UsersEdit = () => {
const handleAdd() => // calls POST to /api/users
return // render input + button
}
Both are displayed together like this in a Server Component Page:
// page.jsx
const Users = () => {
return (
<div>
<UsersList />
<UsersEdit />
</div>
)
}
How should I "reload" or "notify" UsersList that a new user has been added to the collection to force it to display a new user/updated user?
For now, the only way to have the updated data by your Client Component reflected on the Server Component is to call router.refresh(), where router is the returned value by useRouter, after your request to the API. As you can read on the official Next.js doc:
The Next.js team is working on a new RFC for mutating data in Next.js. This RFC has not been published yet. For now, we recommend the following pattern:
You can mutate data inside the app directory with router.refresh().
And they gave a wonderful example, working with a Todo List application. I added it below to have a more complete thread.
Let's consider a list view. Inside your Server Component, you fetch the list of items:
// app/page.tsx
import Todo from "./todo";
async function getTodos() {
const res = await fetch("/api/todos");
const todos = await res.json();
return todos;
}
export default async function Page() {
const todos = await getTodos();
return (
<ul>
{todos.map((todo) => (
<Todo key={todo.id} {...todo} />
))}
</ul>
);
}
Each item has its own Client Component. This allows the component to use event handlers (like onClick or onSubmit) to trigger a mutation.
// app/todo.tsx
"use client";
import { useRouter } from 'next/navigation';
import { useState, useTransition } from 'react';
export default function Todo(todo) {
const router = useRouter();
const [isPending, startTransition] = useTransition();
const [isFetching, setIsFetching] = useState(false);
// Create inline loading UI
const isMutating = isFetching || isPending;
async function handleChange() {
setIsFetching(true);
// Mutate external data source
await fetch(`https://api.example.com/todo/${todo.id}`, {
method: 'PUT',
body: JSON.stringify({ completed: !todo.completed }),
});
setIsFetching(false);
startTransition(() => {
// Refresh the current route and fetch new data from the server without
// losing client-side browser or React state.
router.refresh();
});
}
return (
<li style={{ opacity: !isMutating ? 1 : 0.7 }}>
<input
type="checkbox"
checked={todo.completed}
onChange={handleChange}
disabled={isPending}
/>
{todo.title}
</li>
);
}

Variable storage update across react component

This is a next/react project.
folder structure:
components > Navbar.js
pages > index.js (/ route)(includes Navbar)
> submitCollection.js (/submitCollection)(includes Navbar)
I am trying to have the user submit a specific string as an input and i store it inside the account variable.
Navbar.js
const Navbar = ({}) => {
const [account,setAccount] = useState()
const handleClick = () => {
setAccount(randomNumberThatIHaveGenerated)
}
...
return (
<Link href="/">home</Link>
<Link href="/submitCollection">submit collection</Link>
...
<button onClick={handleClick} >press to set account</button>
...
{account?(<p>{account}</p>):(<p>u need to set an accout</p>)}
)
}
when i visit home using the navbar link, the account is again set to undefineed and i need to press the button again in order to set it. How can i make the string remain set. like persist on the navbar
useState is not persistent, it is bound to its component, in order to make it persist, you have to use localStorage
const [account,_setAccount] = useState();
const setAccount = (val) => {
_setAccount(val);
localStorage.setItem('account', val);
}
useEffect(() => {
const storedAccount = localStorage.getItem('account');
if (storedAccount) _setAccount(storedAccount);
}, [])
const handleClick = () => {
setAccount(randomNumberThatIHaveGenerated)
}
useEffect is called when the component renders, check for stored account and displays it.
And notice how we reimplement setAccount, so that everytime it is called, we update the localStorage.
You can also create a custom hook with this logic, so the component would look cleaner. Or even better, use something like use-state-persist
You can solve this problem using localstorage and useEffect
Adding this piece of code to your work will do the trick
const [account,setAccount] = useState(localStorage.getItem('account') ?localStorage.getItem('account') : null)
useEffect(()=>{
localstorage.setItem(account)
},[account])
For example
const [account,setAccount] = useState(localStorage.getItem('account') ?localStorage.getItem('account') : null)
useEffect(()=>{
localStorage.setItem('account',account)
},[account])
const handleClick = () => {
setAccount(randomNumberThatIHaveGenerated)
}
Hope it helped

Re-render List after deleting item in child component

I want to build a dashboard for a blog. I have a page, listing all blog posts using a component for each list item. Now, inside each list item, I have a button to delete the post.
So far, everything is working. The post gets deleted, and if I reload the page, it is gone from the list. But I can't get it to re-render the page automatically, after deleting a post. I kind of cheated here using window.location.reload() but there has to be a better way?
This is my Page to build the list of all Posts
import {
CCol,
CContainer,
CRow,
CTable,
CTableHead,
CTableRow,
CTableHeaderCell,
CTableBody,
} from "#coreui/react";
import { useState, useEffect } from "react";
import DashboardSidebar from "../../components/dashboard/Sidebar";
import { getAllBlogPosts } from "../../services/blogService";
import BlogListItem from "../../components/dashboard/blog/BlogListItem";
import "./Dashboard.scss";
const AdminBlogListView = () => {
const [blogposts, setBlogposts] = useState([]);
useEffect(() => {
getBlogPosts();
}, []);
async function getBlogPosts() {
const response = await getAllBlogPosts();
setBlogposts(response.data);
}
// console.log(blogposts);
return (
<div className="adminContainer">
<div className="adminSidebar">
<DashboardSidebar />
</div>
<div className="adminContent">
<CContainer fluid>
<CRow className="mb-3">
<CCol>
<CTable>
<CTableHead>
<CTableRow>
<CTableHeaderCell scope="col">#</CTableHeaderCell>
<CTableHeaderCell scope="col">Titel</CTableHeaderCell>
<CTableHeaderCell scope="col">Content</CTableHeaderCell>
<CTableHeaderCell scope="col"></CTableHeaderCell>
</CTableRow>
</CTableHead>
<CTableBody>
{blogposts.map((post) => {
return <BlogListItem key={post._id} post={post} />;
})}
</CTableBody>
</CTable>
</CCol>
</CRow>
</CContainer>
</div>
</div>
);
};
export default AdminBlogListView;
And this is the BlogListItem Component
import { useState, useEffect } from "react";
import {
CTableRow,
CTableHeaderCell,
CTableDataCell,
} from "#coreui/react";
import CIcon from "#coreui/icons-react";
import * as icon from "#coreui/icons";
import {
deleteBlogPost,
getBlogPostById,
// updateBlogPost,
} from "../../../services/blogService";
import { useNavigate } from "react-router-dom";
const BlogListItem = (props) => {
const id = props.post._id;
const [visible, setVisible] = useState(false);
const [post, setPost] = useState({
title: "",
content: "",
});
useEffect(() => {
getBlogPostById(id)
.then((response) => setPost(response.data))
.catch((error) => console.log(error));
}, []);
const handleDelete = async (event) => {
event.preventDefault();
const choice = window.confirm("Are you sure you want to delete this post?");
if (!choice) return;
await deleteBlogPost(post._id);
window.location.reload();
};
return (
<>
<CTableRow>
<CTableHeaderCell scope="row">1</CTableHeaderCell>
<CTableDataCell>{post.title}</CTableDataCell>
<CTableDataCell>{post.content}</CTableDataCell>
<CTableDataCell>
<CIcon
icon={icon.cilPencil}
size="lg"
onClick={() => setVisible(!visible)}
/>
<CIcon
icon={icon.cilTrash}
className="deleteButton"
size="lg"
color=""
onClick={handleDelete}
/>
</CTableDataCell>
</CTableRow>
</>
);
};
export default BlogListItem;
What can I do instead of window.location.reload() to render the AdminBlogListView after deleting an item? I tried using useNavigate() but that doesn't do anything
Thanks in advance :)
You can pass a reference to a function from the parent component AdminBlogListView into the child component BlogListItem, such that it is invoked when a blog post is deleted. That function will have the effect of either repopulating the blog posts or manually removing it from the data (that implementation bit is up to you).
Solution 1: Repopulate all blog posts on deletion
This is a quick fix with a bit of code smell (because you're essentially querying the server twice: once to delete the post and another to fetch posts again). However it is an escape-hatch type of situation and is simple to implement.
When you are rendering BlogListItem, we can pass a function, say onDelete, which will invoke getBlogPosts() to manually repopulate the blog posts from your server:
<BlogListItem key={post._id} post={post} onDelete={getBlogPosts} />
Then it is a matter of ensuring BlogListItem invokes onDelete() when deleting a blog post:
const handleDelete = async (event) => {
event.preventDefault();
const choice = window.confirm("Are you sure you want to delete this post?");
if (!choice) return;
await deleteBlogPost(post._id);
// Invoke the passed in `onDelete` function in component props
props.onDelete();
};
Solution 2: Delete a specific blog post by ID in the parent
Similar to the solution above, but ensure that you are passing a function from the parent that can delete a post by a specific ID (from the argument). This saves you an additional trip to the server.
In your component AdminBlogListView, define a function that can mutate the blogposts state by removing a blog post by ID. This can be done by leveraging functional updates:
const onDelete = (id) => {
setBlogposts((currentBlogPosts) => {
const foundBlogPostIndex = currentBlogPosts.findIndex(entry => entry._id === id);
// If we find the blog post with matching ID, remove it
if (foundBlogPostIndex !== -1) currentBlogPosts.splice(foundBlogPostIndex, 1);
return currentBlogPosts;
})
}
NOTE: The code above assumes that the blog post ID is stored in the _id key. I have simply inferred that from your code, since you have not shared the shape of the data.
Then in your BlogListItem component, it's the same logic as solution #1, but you need to pass the ID into it when invoking it:
const handleDelete = async (event) => {
event.preventDefault();
const choice = window.confirm("Are you sure you want to delete this post?");
if (!choice) return;
await deleteBlogPost(post._id);
// Invoke the passed in `onDelete` function in component props with post ID as an argument
props.onDelete(post._id);
};

How to use same page anchor tags to hide/show content in Next.js

The problem
The current project is using Next.js and this situation occurred: the content needs to be hidden or replaced, matching the current category selected. I want to do it without reloading or using another route to do so. And when the user presses F5 or reloads the page the content remains unchanged.
The attempts
Next.js' showcase page apparently is able to do so. In the docs, there's a feature called 'Shallow routing', which basically gives the possibility to update the URL without realoading the page. That's what i figured out for now. Any clues on how the content is changed to match the category?
Thanks!
You can load the content on the client based on the category passed in the URL fragment (# value) using window.location.hash.
Here's a minimal example of how to achieve this.
import React, { useState, useEffect } from 'react'
const data = {
'#news': 'News Data',
'#marketing': 'Marketing Data',
default: "Default Data"
}
const ShowCasePage = () => {
const router = useRouter()
const [categoryData, setCategoryData] = useState()
const changeCategory = (category) => {
// Trigger fragment change to fetch the new data
router.push(`/#${category}`, undefined, { shallow: true });
}
useEffect(() => {
const someData = data[window.location.hash] ?? data.default // Retrieve data based on URL fragment
setCategoryData(someData);
}, [router])
return (
<>
<div>Showcase Page</div>
<button onClick={() => changeCategory('news')}>News</button>
<button onClick={() => changeCategory('marketing')}>Marketing</button>
<div>{categoryData}</div>
</>
)
}
export default ShowCasePage

How to optimize code in React Hooks using memo

I have this code.
and here is the code snippet
const [indicators, setIndicators] = useState([]);
const [curText, setCurText] = useState('');
const refIndicator = useRef()
useEffect(() => {
console.log(indicators)
}, [indicators]);
const onSubmit = (e) => {
e.preventDefault();
setIndicators([...indicators, curText]);
setCurText('')
}
const onChange = (e) => {
setCurText(e.target.value);
}
const MemoInput = memo((props)=>{
console.log(props)
return(
<ShowIndicator name={props.name}/>
)
},(prev, next) => console.log('prev',prev, next)
);
It shows every indicator every time I add in the form.
The problem is that ShowIndicator updates every time I add something.
Is there a way for me to limit the the time my App renders because for example I created 3 ShowIndicators, then it will also render 3 times which I think very costly in the long run.
I'm also thinking of using useRef just to not make my App renders every time I input new text, but I'm not sure if it's the right implementation because most documentations recommend using controlled components by using state as handler of current value.
Observing the given sandbox app behaviour, it seems like the whole app renders for n times when there are n indicators.
I forked the sandbox and moved the list to another functional component (and memo'ed it based on prev and next props.
This will ensure my 'List' is rendered every time a new indicator is added.
The whole app will render only when a new indicator is added to the list.
Checkout this sandbox forked from yours - https://codesandbox.io/embed/avoid-re-renders-react-l4rm2
React.memo will stop your child component rendering if the parent rerenders (and if the props are the same), but it isn't helping in your case because you have defined the component inside your App component. Each time App renders, you're creating a new reference of MemoInput.
Updated example: https://codesandbox.io/s/currying-tdd-mikck
Link to Sandbox:
https://codesandbox.io/s/musing-kapitsa-n8gtj
App.js
// const MemoInput = memo(
// props => {
// console.log(props);
// return <ShowIndicator name={props.name} />;
// },
// (prev, next) => console.log("prev", prev, next)
// );
const renderList = () => {
return indicators.map((data,index) => {
return <ShowIndicator key={index} name={data} />;
});
};
ShowIndicator.js
import React from "react";
const ShowIndicator = ({ name }) => {
console.log("rendering showIndicator");
const renderDatas = () => {
return <div key={name}>{name}</div>;
};
return <>{renderDatas()}</>;
};
export default React.memo(ShowIndicator); // EXPORT React.memo

Categories

Resources