Update server-side rendered props dynamically - reactjs

I've got posts with comments and like to implement a post view that includes all comments to that post.
My getServerSideProps passes the post (including all comments) to my page. Whenever a new comment is written the comments should be dynamically updated, but I'm currently facing some problems with that.
My post view:
const PostView: NextPage = ({ post }) => {
return (
<Layout>
{post.title}
<CommentList initialComments={post.comments} postId={post.id} />
</Layout>
);
};
export default PostView;
export const getServerSideProps = () => {
const post = await getPost(); // returns the post and all its comments
return { props: { post } };
};
The CommentList component:
const CommentList = (initialComments, postId) => {
const { data: comments } = useQuery(["comments", postId], async () => getComments(), { initialData: initialComments);
return (
<>
Comments: {comments.length}
... new comment form ...
... list of comments ...
</>
);
}
The reason why I still want to query comments with react-query is simple: comments should be server-side rendered so that they become seo-relevant, while I want human users to get a dynamic list that can be updated.
When writing new comments I update the QueryClient of react-query by hand:
export const useCreateCommentMutation = (postId: string) => {
const queryClient = useQueryClient();
return useMutation(
["comments", postId],
async (values) =>
await axios.post("/api/comments", values),
{
onSuccess: async res => {
queryClient.setQueryData<CommentWithAuthor[]>(
["comments", postId],
prev => [...(prev || []), res.data],
);
},
},
);
};
This seems to work at first glance; when I check the DOM the comments are included and when writing new comments they dynamically appear.
Unfortunately, when I refresh the page I get the following error:
Text content did not match. Server: "3" Client: "4"
3 (or 4) in this case is the comments.length output.
What am I doing wrong in this case?
Thanks
Edit 1:
I've also tried fixing it by using useEffect:
const [usedComments, setUsedComments] = useState([]);
useEffect(() => {
setUsedComments(comments || initialComments);
}, [comments])
And render usedComments instead - but unfortunately now the comments are no longer part of the DOM.

Why don't you try using useState() hook to store that the Comment data . Every time useQuery runs it will update the state which will cause re-rendering of the comment and also.
I can't think of anything other then this. I don't know what your comment json/data look like to do the server side dynamic rendering.
And useQuery runs on user action like click on add new comment button or a time loop.
and your error seems like It is caused by some Server and client attribute of component.

Related

re-updating the page state

There are 2 pages: the first loads an array of posts with a jsonplaceholder, the second displays the body of a specific post by clicking on the post.
When I return from the post page to the page with all the posts, the data is updated and re-requested.
How can I save the page state?
const [data, setData] = useState([])
const [posts, setPosts] = useState([])
const getPostsData = async () => {
try {
const postsData = await getData('https://jsonplaceholder.typicode.com/posts')
setData(postsData)
} catch (error) {
console.log(error.message)
}
}
useEffect(() => { getPostsData() }, [])
useEffect(() => { setPosts(data) }, [data])
posts.map(post => <Link to={`${post.id}`} key={post.id}><li className="list-group-item" >{post.title}</li></Link>)
I use 2 states. One is for loading data, and the second is for displaying. I need it for sorting and searching.
I think for your case it's better to use redux , context or react-query, but if you don't want to, you must avoid using Link for the child component because when you change route your previous state will be removed
you can simply show post detail as a modal or part of the main page

fetching data in React with DynamicRoutes, nextJS and useEffect

I have a blog link. Link is dynamic route with blog id. It's the Link wrapper from Next.
//link
<h3 className="blogTitle">
<Link href="[blog]" as={props.item.postingId}>{props.item.title}</Link>
</h3>
Now I want to pass "blog id" to the component and to present data in a new page.
//page where link leads to
const ad = () => {
const router = useRouter()
const {
query: {blog},
} = router
const [data, setData] = useState(false);
const [loading, setLoading] = useState(false);
console.log('....outside useEffect log', blog)
useEffect(() => {
console.log('useEffect consolelog', blog);
axios.get('httpwww.blogapiadress.com/'+ ad)
.then(response => setData(response.data))
.then(setLoading(false))
}, [])
return(
<Container fluid className="padding0">
/// data should be here.
</Container>
);
}
export default ad;
Problem: in useEffect console.log('blog', blog) returns undefined, so router does not return value from query. However, outside of useEffect it does. How to solve that issue, I want to fetch data related to the router query?
Since axios is getting undefined instead of blog id, I am getting 404.
You can use getStaticProps() to fetch the blog data at build time.
Example:
// posts will be populated at build time by getStaticProps()
function Blog({ posts }) {
return (
<ul>
{posts.map((post) => (
<li>{post.title}</li>
))}
</ul>
)
}
// This function gets called at build time on server-side.
// It won't be called on client-side, so you can even do
// direct database queries. See the "Technical details" section.
export async function getStaticProps() {
// Call an external API endpoint to get posts.
// Access route params:
const blog = context.query.blog // or context.params.blog for parametrized routes
return {
const res = await fetch('https://...')
const posts = await res.json()
// By returning { props: { posts } }, the Blog component
// will receive `posts` as a prop at build time
return {
props: {
posts,
},
}
}
export default Blog
More info on NextJS docs.
I don't think you need to use the global window object to access dynamic data related to your route -- you should be able to use the Next router. I think the way you are defining href -- the only required prop for Link is causing issues. Looking at docs and your current exampel you probably want to use something like:
<Link
href={{
pathname: '/[blog]',
query: { blog: props.item.postingId },
}}
>
<a>{props.item.title}</a>
</Link>
// or
<Link href={`/${encodeURIComponent(props.item.postingId)}`}>
<a>{props.item.title}</a>
</Link>
Then you should be able to properly access [blog] (i.e., your postingId) using Router. For example, if your route was defined dynamically by /[blog].js, you could use the following:
import { useRouter } from 'next/router'
const ad = () => {
const router = useRouter()
const { blog } = router.query
const [data, setData] = useState(false);
const [loading, setLoading] = useState(false);
useEffect(()=>{
axios.get('httpwww.blogapiadress.com/'+ blog)
.then(response => {
setData(response.data)
setLoading(false)
})
}, [])
if (loading || !data) return <div> Loading... </div>
return(
<Container fluid className="padding0">
/// Render data
</Container>
);
}
export default ad;
Looking for answer for a few hours, and when I posted question on stack.. I figured out the answer.
So problem was that query is empty with next static generation at build time https://nextjs.org/docs/api-reference/next/router#router-object
I havent found best solution, but i found working one.
I got blog id from the windows.location href
useEffect(()=>{
const last = window.location.href.split('/').pop();
console.log('last', last)
axios.get('https://blogpostings.com/'+last)
.then(response => setData(response.data))
}, [])
I am not sure if its proper or good way, but it works.
I hope someone will find this helpful.
If you want get the query parameter from the link, you need to insert at the end of the url: https://sample-link.com?blog=123
Then same as your code above:
import { useRouter } from 'next/router'
const router = useRouter()
const {
query: {blog}
} = router;
console.log("blog: ", blog)
Result:
blog: 123

Redux: Why does my useEffect() keeps rerendering its value on every page rerender

I am learning react-redux.
I got the following problem:
I make two async api calls (with redux-thunk):
the first one to fetch country names (in one object, ex: {countries: [{...}, ...]}.
Those country names I use afterwards to make a second api call, to get all the soccer leagues, that are in those countrys (sometimes, there are none, so I get a null). In this case, the call is made with each countryName separatly. I make out of the results an array.
This arrays length is 255m out of which I filter out the null values and map the leagues names.
After I click on a League's name, a page is rendered ({Link} from "react-router-dom";).
NOW my problem occurs
When I click, to get back to my home page (<Link to={"/"} >), both useEffect() are making an api call again. Why?
Here is the code for my useEffect():
const dispatch = useDispatch();
const selectAllCountries = useSelector(state => state.allCountries);
const selectAllLeagues = useSelector(state => state.allLeagues);
useEffect(() => {
dispatch(allCountries());
}, [dispatch]);
useEffect(() => {
if(!_.isEmpty(selectAllCountries.data)) {
selectAllCountries.data.countries.map(el => dispatch(allLeagues(el.name_en)));
}
}, [dispatch, selectAllCountries.data]);
I tried to make a custom hook and put the useEffect() in there:
const useCountries = getCountries => {useEffect(() => {
dispatch(getCountries());
},[getCountries])}
useCountries(allCountries);
as suggested here:
React hooks: dispatch action from useEffect
But it didnt help.
Will be greatful for any help.
ANSWER:
in "./actions/.../allLeagues.js
...
import _ from "lodash";
export const allLeagues = (country) => async (dispatch, getState) => {
if (!_.isEmpty(getState().allLeagues) && !_.isEmpty(getState().allLeagues.data)) {
return Promise.resolve();
} else {
try {
...
}
}
}
Question, that was also helpfull:
Fetching data from store if exists or call API otherwise in React
(take look at answer about getStore())
As mentioned in a comment above, the homepage unmounts when you click to go to a new page. When you go back, the page re-mounts and the effect runs again, triggering another API call. You can prevent the API call by checking whether or not the values already exist in your store. I personally like to do this in the action creator, but you could do it in the effect as well.
Checking state in the action creator:
function allLeagues(countryName) {
return (dispatch, getState) => {
// Call `getState` and check whether `allLeagues` has been populated yet.
const { allLeagues } = getState();
if (allLeagues && allLeagues.data && allLeagues.data.length) {
// You already have the data, no need to make the API call.
return Promise.resolve();
}
// No data, make the API call...
};
}
Checking state in the effect:
useEffect(() => {
// Check whether the league data is set or not.
if(!_.isEmpty(selectAllCountries.data) && _.isEmpty(selectAllLeagues.data)) {
selectAllCountries.data.countries.map(el => dispatch(allLeagues(el.name_en)));
}
}, [dispatch, selectAllCountries.data, selectAllLeagues.data]);

Can't render data from API being passed down as props (ReactJS)

I'm really stuck in trying to render some data being passed down as props. I'll include some code and definitions below, but if you feel that I need to include some further code snippets, please let me know (I'm really struggling to find what's causing the error, so I may have missed out the causal issue!).
I first take data from an API which is then used to populate a UserList component via useState (setUsers(data):
useEffect(() => {
async function getUserList() {
setLoading(true);
try {
const url =
"API URL";
const response = await fetch(url);
const data = await response.json();
setUsers(data);
} catch (error) {
throw new Error("User list unavailable");
}
setLoading(false);
}
getUserList();
}, []);
If a user is clicked in the UserList, this changes the selectedUser state of the parent Home component to be the specific user's unique_ID via:
onClick={() => setSelectedUser(unique_ID)}
If the selectedUser changes, the Home component also does a more updated data fetch from the API to get all information relevant to the specific user via their unique_ID:
useEffect(() => {
async function getSelectedUserData() {
try {
const url = `API URL/${selectedUser}`;
const response = await fetch(url);
const data = await response.json();
setSelectedUserData(data);
} catch (error) {
throw new Error("User data unavailable");
}
}
getSelectedUserData();
}, [selectedUser]);
The specific user data is then passed down as props to a child UserInformation component:
<UserInformation selectedUser={selectedUser} selectedUserData={selectedUserData} />
At this point, I can see all the data being passed around correctly in the browser React Developer Tools.
The UserInformation component then gets the data passed via props:
import React, { useEffect, useState } from "react";
function UserInformation({ selectedUser, selectedUserData }) {
const [currentUser, setCurrentUser] = useState({ selectedUserData });
useEffect(() => {
setCurrentUser({ selectedUserData });
}, [selectedUser, selectedUserData]);
return (
<div>
<p>{selectedUserData.User_Firstname}</p>
<p>{currentUser.User_Firstname}</p>
</div>
);
}
export default UserInformation;
And here is where I get stuck - I can't seem to render any of the data I pass down as props to the UserInformation component, even though I've tried a few different methods (hence the <p>{selectedUserData.User_Firstname}</p> and <p>{currentUser.User_Firstname}</p> to demonstrate).
I'd really appreciate any help you can give me with this - I must be making an error somewhere!
Thanks so much, and sorry for the super long post!
I managed to solve this (thanks to the help of Mohamed and Antonio above, as well as the reactiflux community).
import React from "react";
function UserInformation({ selectedUserData }) {
const currentUserRender = selectedUserData.map(
({ User_Firstname, User_Lastname }) => (
<div key={unique_ID}>
<p>{User_Firstname}</p>
</div>
)
);
return (
<div>
{selectedUserData ? currentUserRender : null}
</div>
);
}
export default UserInformation;
As selectedUserData was returning an array instead of an object, I needed to map the data rather than call it with an object method such as {selectedUserData.User_Firstname}.
const currentUserRender = selectedUserData.map(
({ User_Firstname, User_Lastname }) => (
<div key={unique_ID}>
<p>{User_Firstname}</p>
</div>
)
);
The above snippet maps the selected data properties found inside selectedUserData ({ User_Firstname, User_Lastname }), with the whole map being called in the return via {selectedUserData ? currentUserRender : null}.
Hopefully my explanation of the above solution is clear for anyone reading, and a big thanks again to Mohamed and Antonio (as well as a few others in the reactiflux Discord community) for helping!
You're trying to set the current user to an object with key "selectedUserData".
So if you want to access it you've to access it by this key name so change this line currentUser.User_Firstname to currentUser.selectedUserData.User_Firstname

next.js mapStateToProps, mapDispatchToProps and getInitialProps

i am currently still trying to wrap my head around redux when using next.js and i am not sure what is the best way to use redux with next. I am used to using mapDispatchToProps for my actions and mapStateToProps for my props. After some research i am now using next-redux-wrapper in my _app.js like recommended but now i am fighting with how to best get my props and dispatch my actions. I had look at a few examples and practices and now have a counter component based on one of these examples.
class Counter extends Component {
increment = () => {
const {dispatch} = this.props
dispatch(incrementCount())
}
decrement = () => {
const {dispatch} = this.props
dispatch(decrementCount())
}
reset = () => {
const {dispatch} = this.props
dispatch(resetCount())
}
render () {
const { count } = this.props
return (
<div>
<h1>Count: <span>{count}</span></h1>
<button onClick={this.increment}>+1</button>
<button onClick={this.decrement}>-1</button>
<button onClick={this.reset}>Reset</button>
</div>
)
}
}
function mapStateToProps (state) {
const {count} = state.counter;
return {count};
}
export default connect(mapStateToProps)(Counter)
Most examples i have seen so far do something similar to this or only dispatch actions in getInitialProps. Is there a reason to do it this way and not use mapDispatchToProps?
Cause this work perfectly fine as well:
export default connect(null, {authenticate})(Signin);
Dispatching actions in getIntialProps seems to have some drawback (or i made some mistakes), cause they do not get executed again when the props change. In my user-profile component i get the current user based on a token from the redux store like this:
const Whoami = ({isAuthenticated, user}) => (
<Layout title="Who Am I">
{(isAuthenticated && user && <h3 className="title is-3">You are logged in as <strong className="is-size-2 has-text-primary">{user}</strong>.</h3>) ||
<h3 className="title is-3 has-text-danger ">You are not authenticated.</h3>}
</Layout>
);
Whoami.getInitialProps = async function (ctx) {
initialize(ctx);
const token = ctx.store.getState().auth.token;
if(token) {
const response = await axios.get(`${API}/user`, {headers: {
authorization: token
}});
const user = response.data.user;
return {
user
};
}
}
const mapStateToProps = (state) => (
{isAuthenticated: !!state.auth.token}
);
export default connect(mapStateToProps)(Whoami);
This works perfectly fine for the initial page-load or when navigating there one the client, but when the token expires or i logout the page does not reflect that without reload or navigating there again without my mapStateToProps. But it seems super clunky to split the concern over 2 seperate functions. But i cant find a cleaner way to do it.
Thanks in advance
About mapDispatchToProps:
It is better to use mapDispatchToProps at least because it is easier to test: you can just pass a mock function to your component. With using this.props.dispatch to dispatch some imported actions it can be much harder.
About getInitialProps:
This answer may be helpful:
GetInitialProps: is provided by Next.js and it is NOT always triggered, so be careful with that, it happen when you wrap 1 component inside another. If the parent Component has GetInitialProps, the child's GetInitialProps will never be triggered, see this thread for more info.
I found some answers to my questions after playing around with next a bit more. For pages where the data does not change after intial load, i could get rid of mapStateToProps by rewriting my thunks a bit to return the dispatches and only use getInitialProps like this:
export function fetchShow(id) {
return (dispatch) => {
dispatch({ type: actionTypes.FETCH_SHOW_REQUESTED,id});
// we need to return the fetch so we can await it
return fetch(`http://api.tvmaze.com/shows/${id}`)
.then((response) => {
if (!response.ok) {
throw Error(response.statusText);
}
//dispatch(itemsIsLoading(false));
return response;
})
.then((response) => response.json())
.then((data) => dispatch({type: actionTypes.FETCH_SHOW_SUCEEDED,id, show: data, time: Date.now() }))
.catch(() => dispatch({ type: actionTypes.FETCH_SHOW_ERROR,id }));
};
}
Post.getInitialProps = async function ({store, isServer, pathname, query}) {
const { id } = query;
const {show} = await store.dispatch(fetchShow(id));
return {show};
}
For pages where the data should update upon store changes i am not sure yet. My current idea is to try and write a helper function that will be called from both getInitialProps and mapStateToProps to reduce code duplication but i am not sure yet.

Resources