Implementing a local like/unlike feature in NextJS and making it persist between client-side routes - reactjs

I have a blog front-end app built using NextJS and it looks like this:
Each card here is a functional component called PostPreview.jsx. As you can see, each component comes with a heart icon. By default, this icon is to stay gray. However, when clicked it turns red, signifying that the post has been liked. This action only occurs if the user is logged in. If not, clicking the heart icon presents a login modal.
Right now, I'm only focussing on making the "like" persist between client-side navigations, i.e. without any interaction with the db/server.
As of now, clicking the icon, toggles the color alright. However, it fails to persist when you navigate away, say, by clicking on a post title and then hitting the back button to return to this page. What is the recommended way to achieve this functionality?
The entire codebase can be found at my repo here: https://github.com/amitschandillia/proost/tree/master/web
The code for the component in question (PostPreview.jsx) is at: https://github.com/amitschandillia/proost/blob/master/web/components/blog/PostPreview.jsx
The site is live at https://www.schandillia.com/blog.
I understand I could use Redux, but not sure how to prevent the value from being reset upon each re-render even when using Redux.
Illustrating the problem better
Visit blog page; several instances of component (PostPreview) render for the first time:
Receive array of post "likers" via the likedBy prop object.
Retrieve logged-in user's ID from the Redux store via userInfo.userID.
Look up userInfo.userID against the likedBy.readers array of IDs.
If user ID exists in readers array, post is liked, set liked to error to turn the heart icon red and push post's id to the likedPosts redux store.
If user ID doesn't exist in readers array, leave liked to inherit to leave it gray and remove post's id from the likedPostsredux store.
Like a post; click the heart icon:
Set liked to error to turn the heart icon red.
Push post's id to the likedPostsredux store.
Unlike a post; click the heart icon:
Set liked to inherit to turn the heart icon red.
Remove post's id from the likedPostsredux store.
Now click any link on page to navigate away from the page (client-side routing, no server contact here). Then hit the browser's back button to return to the blog page.
At this point, the component (PostPreview) re-renders and the redux store will be reset in accordance with the original likedBy prop object. This, of course, means that all the changes since the first render are gone. This is where I need help. How would you handle such a situation where user interactions like likes and dislikes have to be persisted across client-side navigations and also honored across re-renders?

I see two ways:
1) Simple: By using local storage you can write array of likes
likes: [likedPostId1, likedPostId2, ...]
And then in PostPreview check if current card id included in likes array
let isLiked = likes.includes(currentPostId);
2) Right: By using Redux, it's the same way, but you'll store likes array in Redux and use for page navigation react-router.
It's essential to understand how navigation works in SPA

For simple SPA implementation:
Simple solution would be to just use Context API with hooks (refresh will reset it)
1a. Another simple solution is to use LocalStorage with hooks (refresh or back button will persist it)
Than you could refactor it into REDUX (a lot of boilerplate)
2a. so maybe you could just use GraphQL as an app state
Here is an article explaining how to do that highlighting Redux vs hooks with Context API:
https://www.sitepoint.com/replace-redux-react-hooks-context-api/
Also it's worth to note that REDUX or GraphQL will not persist it itself between refreshes

4. If user ID exists in readers array, post is liked, set liked to error to turn the heart icon red and push post's id to the likedPosts redux store.
5. If user ID doesn't exist in readers array, leave liked to inherit to leave it gray and remove post's id from the likedPosts redux store.
You should wrap #4. and #5. as one action in redux store.
const mapDispatchToProps = (dispatch) => {
return {
onUpdateLikedPosts: (data) => dispatch(actions.updateLikedPostsAction(data))
};
};
And, Execture the function at when component of PostPreview is mounted.
const componentDidMount() {
this.props.onUpdateLikedPosts(data)
}

Related

How to not lose my information in props when i reaload my pages?

I'm new in react and i'm studing a method to make connections with Firebase to my Project.
With props I can pass an ID and search it in firebase, but when i reload my page, all the props are lost and i have this message in the image:
The value is Undefined because the props are losing their values
Is there any other way for this information don't lost?
I dont try anything because idk what to do.
The ID in the props will be used in useEffect() on my page.
Edit:
There is the Cards Image:
Cards
And here it's when i click on the card "Pinscher":
The page when i click on Pinscher
But when i Reload the page "Pinscher", i lost the Id passed with props.
The most straightforward way I know to persist data over a reload is to save it to localstorage or sessionstorage. Which one to use--and whether it's appropriate--depend on your actual use case, which we can't comment on without more knowledge of your project.
Assuming you did want to use that, a basic implementation would be to write helper functions when you set state to also save the data to storage. Your initial state load would then look for storage information as its default and then fallback to null if it can't find anything in storage.
const [arbData, setArbData] = useState(window.localStorage.getItem('arbData'));
const setArbDataWrapper = (data) => {
setArbData(data);
window.localStorage.set('arbData', data);
}

How to share on social media with logged user content?

I'm new using React, so please excuse me for the basic question. I didn't know where to find the right answer (or even how to ask the proper question). My doubt is: I have a React app where the user will have his content, which is basically a text and an image (using base64). He must be logged in to access it. I'm saving the content inside Firebase, as a JSON file with logged user.id generated by Firebase, as the category, and each user content is composed of an UUID I generate, the image (base64), and a text.
So, main page I list all the content for each logged user, and I want to put a button there so (s)he can share his content on Facebook, for instance. This "content" I'm calling a user story. So, for each listed story I create a Link to load this particular story in another page:
arrayOfStories.map((story) => (
(...)
<Link className="logo-container" to='/loadedstory' state={story}>
))
I'm passing the story object, which is basically the JSON with storyText, image, and uuid properties in a useLocation() state.
Then, from my loadedstory.component.jsx context page, I do this:
const LoadedStory = () => {
const {state} = useLocation();
(...)
return (
<>
<div>
{ state && state.uuid ? (
<div className="story-content"><img alt="Your content!" src={`data:image/png;base64,${state.image}`} className='resize-image'></img></div>
<div className="story-content"><p><span className='stroke'>{state.storyText}</span></p></div>
(...)
):
(<div></div>)}
</div>
(...)
</>
As you can see, if the user wants to share this particular story on Facebook, the base URL link will be /loadedstory and won't load anything without the logged user context from useLocation().
What's the best practices for developing such shareable content within a logged context?
I was thinking about to do something like /loadedstory/:uuid to use the story UUID from each story and query Firebase everytime this context is needed, but the problem is my Firebase category is the user.id not the story.uuid. Would I need to query the whole users in the database then for this UUID? Is this the right approach?
If you 're using NextJS, you could probably keep the data as-is and generate a static page for every user and all their stories, and the pages pointing to those stories.
You could pass the user-id and story-id to the story page. That way the story can fetch the (all) the user's data and sift through it to find the story-id. If you use something like ReactQuery, it will cache so you won't feel so bad about asking for data you already have.
As far as 'best practice', generally you would make a new DB table to store each story as its own record with the user-id as an owner. You use something like GraphQL to fetch only a user, only a story, or a user and their first 25 stories.
You also need a "router" to facilitate navigating around (pages or content) without re-inventing the wheel.
One very popular solution is to build the app using NextJS. It's easy to get up and running and their starter example covers navigation.
Many projects use React Router v6 and it's well worth running through their tutorial whether you'll end up using it or not.
If you want extra type safety, check out the TanStack Router. It's new and built on the shoulders of many great ideas that preceded it.

React don't mount component until needed but don't unmount afterwards in list of components

Say I am building an instant messaging with app with React (I'm not doing that exactly, but this is easier to explain). I have a sidebar with a list of conversations and, when you click one, it is shown on the right (similar to this). I don't want to mount each conversation component until the user clicks it, but I don't want to unmount it, just hide it, when they click on another conversation. How can I do this cleanly? There will never be more than about 30 chats for any user.
You can store the enabled conversations in an array that you use to show, and when you disable a conversation you can just add a hidden prop to it which you pass to the conversation and make it return null. This will make it not render anything but will not unmount it since you have not removed it from the array that handles the display of conversations.
example at: https://codesandbox.io/s/wispy-forest-59bqj
This is a bit hard to answer since you haven't posted the code.
But, theoretically, the best way to approach this problem is to transfer the data from your sidebar component and load it onto the right component on a per-user basis. You don't have to mount each "conversation component".
You can do this by with the boolean hidden property in your markup. React will render as usual and simply pass it along to the html, the browser will then simply not paint it.
const HideMe = ({ isHidden }) => (
<div hidden={isHidden}>
can you see me?
</div>
)
I made an example for you:
https://codesandbox.io/s/elastic-curie-t4ill?file=/src/App.js
reference: https://www.w3schools.com/tags/att_hidden.asp

How to update match.params?

The react app has search page. There are input.
The path is 'search/:query', and by default you see zero results.
If you go to 'search/star%20wars' you will see some results. In componentDidMount() I added if statement to load result if match.params.query is not null.
If I type into search input Spider Man and click submit - I trigger a search and show results. But if you reload page - you will see the result about Star Wars. So how update match.params.query? Or may be there other solution of fix this.
You need to update the history object as well.
What you are doing is altering the history object available to you and calculating the results based on that object. But when you will refresh the page it still holds the original history object.
One way of doing it, you need to push or replace a new route in the history.
Because evert search page is a new page, so if you want the previous pages to stay preserved you should use history.push otherwise history.replace
Implement it like this:
var routeObj = {
pathname: samePath,
state: sameState,
query: newQuery
}
//push it in your history using which ever routing library you are using.
//For Example:
router.history.replace(routeObj);
Note: Do not worry about rendering speed on changing the history. React is smart enough to handle that. Basically whenever you will push a route whose component is already mounted it will not unmount and remount the same component again, rather it will just change the props and will re render it.
The callback for this case will be => componentWillReceiveProps
#misha-from-lviv The way I see your problem statement is that you have two source of truth on is the query params, using which you should update your state, and the other is the default state which is populated from the default value of your filters.
As #Akash Bhandwalkar suggested, you do need to update the route in using the History API. But also you also a need a top-level orchestrator for your application state, which will allow you to read and write to the history api ( change your route ) and also do an XHR / fetch for you to get the results.
How I'd approach this is that I'd start with a Parent component, namely FiltersContainer , which actually does this orchestration to read and write to the url. This Container would have all the side-effect knowledge for fetching and updating the routes ( error handling included ). Now the all the child components ( filters and search results maybe ) will just read the state thus orchestrated and re-render.
Hope this guides your thinking. Do revert here if you need further guidance. 😇
Cheers! 🍻

How to deal with query params in react + react-router + flux

I'm trying to replace a Backbone.Marionette App to React and am facing difficulty thinking about query params. I think I'm missing a really simple peace in understanding this pattern so I apologize if this question is totally nonsense. I would appreciate any support or just pointing me to some direction that I can google more specifically.
There's a /users page which lists users and you can filter the users via search bar. So if you want to filter the users which contain 'joe' in their username, I would make a request to the server with query params like /users?username=joe. In addition I am able to paginate by adding a page parameter, too (/users?username=joe&page=1).
If I only think about the functionality, the flow would probably be
The Client inserts joe to the input element and clicks Search.
Clicking the Search button fires an Action (like Action.getUser).
The Action makes a request to the server and receives the results
The Dispatcher dispatches a function with the results payload to whomever (usually the Store) is interested in the Action.
The Store's state changes with the new result received by the Action
The View (Component) re-renders by listening to the Store's change.
and it works as expected. However, I would like the Client to be able to bookmark the current filtered result and be able to come back to the same page some time later. This means I will need somewhere to save explicit information about the search term the Client made, which is usually the url (am I right?). So I will need to update the url with query parameters to save the search term (/users?username=joe&page=1).
What I'm confused is where and when to update the url? What I can come up with right now are the 2 options below - and they don't seem to be clean at all.
Option 1
The Client inserts joe to the input element and clicks Search.
Clicking the Search button fires a transition of the ReactRouter with the new query params (/users?username=joe&page=1).
The View (Component) receives the new params via this.props.params and this.props.query.
The View (Component) fires an Action like Action.getUser depending on the query params it receives - in this case username=joe&page=1.
after this, it is the same as above
Option 2 (only 6 is different from what I explained above)
The Client inserts joe to the input element and clicks Search.
Clicking the Search button fires an Action (like Action.getUser).
The Action makes a request to the server and receives the results
The Dispatcher dispatches a function with the results payload to whomever (usually the Store) is interested in the Action.
The Store's state changes with the new result received by the Action
The View (Component) re-renders by listening to the Store's change. And somehow (I don't know how, yet) updates its url depending on its props (like this.props.searchusername, and this.props.searchpage)
What is the best practice on handling query params? (or this may not be specific to query params)
Am I completely misunderstanding the design pattern or architecture? Thanks in advance for any support.
Some articles I've read
Any way to get current params or current query from router (outside of component)?
Async data and Flux stores
Make it easier to add query parameters
React Router and Arbitrary Query Params: Page Refreshes Unintentionally on Load?
Add default params?
I would consider best practice to be the submit button only setting the location query (username). The rest should be taken care by the main react component that is assigned as router component. By this, you can be sure that anytime one revisits or shares the url, they can get the same results. And this is very generic too.
Something like this:
let myQuery = this.props.location.query;
if (myQuery.username) {
let theUser = myQuery.username;
this.setState({
userName = myQuery.username
});
} else {
this.setState({
userName = false //Show All
});
}
And then use this state "userName" to send to the server to search with. By this way, you will not need to iterate the code of the component that takes care of listing users since server already sends the relevant data.
In my experience with using location queries in React, I have been very content with their reactivity cycles and performance. I'd highly recommend keeping every different app state in relevance with the url.
Not entirely sure what you mean by
this means I will need to update the url to save the information (/users?username=joe&page=1).
You will probably have a similar structure to this.
TopContainer.jsx
-- Users.jsx
-- a list of User.jsx
Usually TopContainer will watch all the stores and if anything changed, pass it down to users.jsx. That way in Users.jsx, you can simply render this.props.users without worrying about any reRendering.
The search users actions usually happens in TopContainer's componentWillMount event, and you the page will listen to UserStore. That's a good place to throw in any query params. Something like this would work
componentWillUnmount() {
let searchTerm = router.getCurrentQuery().searchTerm;
UserActions.searchUsers(searchTerm)
},
The page doesn't really care if the url has a query params or not, it just dumbly shows whatever in the user store.
Then when the search finishes, Users.jsx will be reloaded and show the correct results

Resources