React Ionic Navigation with directly passed functional props - reactjs

How do I push a page using React and Ionic with functional props?
I did try:
history.push({
pathname: '/edit_some_property',
state: {
onSetSomeProperty(val) { setSomeState(val) }
}
})
But that throws:
DOMException: Failed to execute 'pushState' on 'History': onSetSomeProperty(val) { setSomeState(val); } could not be cloned.
Apparently, state is serialized to a string, so passing functions isn't going to work.
Or is there another approach that would work better for pushing screens with callbacks to the previous screen?
Background and Detail:
I have a very hierarchical style React + Ionic app.
Choose an item from a list, push a screen with item details.
Choose a sub item from a list, push a screen with sub item details.
Tap to edit an attribute, push a screen with form fields, and callback to the previous screen with the result.
Now it appears that in order to navigate from screen to screen, Ionic requires you to use React Router, where each page has a unique URL. But this doesn't work so well here since each screen depends on the state of previous screens.
This means that there is no apparent way to communicate rich props between pages. If I have a callback to the previous page, I can't pass that along because I don't have direct access to component being pushed with the router in the way.
There's also ion-nav https://ionicframework.com/docs/api/nav which is closer to what I think I need, but that doesn't seem to have a react interface at all.
I tried:
<IonNav root={Home} />
Which throws this, deep in obfuscated internals:
Error: framework delegate is missing

My original answer was way off base sorry. I did some testing, and found that anything passed to state needs to be serializable. In order to do this for a function, others have reported that https://www.npmjs.com/package/safe-json-stringify helped them out.

Related

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

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

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)
}

Using this.props.history.push("/path") is re-rendering and then returning

Edited the question after further debugging
I am having a strange issue, tried for a while to figure it out but I can't.
I have a React Component called NewGoal.jsx, after a user submits their new goal I attempt to reroute them to my "goals" page.
The problem: After they submit the browser loads in my goal page, but only for one second. It then continues and goes BACK to the NewGoal page!!
I am trying to understand why this is happening, I am beginning to feel that this might be an async issue.
Here is my code, currently it is using async-await, I also tried the same idea using a .then() but it also didn't work:
async handleSubmit(event)
{
const response = await axios.post("http://localhost:8080/addGoal",
{
goalID: null,
duration: this.state.days,
accomplishedDays: 0,
isPublic: this.state.isPublic,
description: this.state.name,
dateCreated: new Date().toISOString().substring(0,10),
}) */
// push to route
this.props.history.push("/goals");
}
While debugging, I tried taking out the functionality where I post the new message, and just did a history.push, code is below - and this completely worked.
// THIS WORKS
async handleSubmit(event)
{
// push to route
this.props.history.push("/goals");
}
But as soon as I add anything else to the function, whether before the history.push or after, it stops working.
Any advice would be very very appreciated!
Thank you
In the React Router doc's the developers talk about how the history object is mutable. Their recommendation is not to alter it directly.
https://reacttraining.com/react-router/web/api/history#history-history-is-mutable
Fortunately there are few ways to programmatically change the User's location while still working within the lifecycle events of React.
The easiest I've found is also the newest. React Router uses the React Context API to make the history object used by the router available to it's descendents. This will save you passing the history object down your component tree through props.
The only thing you need to do is make sure your AddNewGoalPage uses the history object from context instead of props.
handleSubmit(event)
...
//successful, redirect to all goals
if(res.data)
{
this.context.history.push("/goals")
}
...
})
}
I don't know if you're using a class component or a functional component for the AddNewGoalPage - but your handleSubmit method hints that it's a member of a Class, so the router's history object will be automatically available to you within your class through this.context.history.
If you are using a functional component, you'll need to make sure that the handleSubmit method is properly bound to the functional component otherwise the context the functional component parameter is given by React won't not be available to it.
Feel free to reply to me if this is the case.

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! 🍻

Does React have keep-alive like Vue js?

I made a Todo list with React js. This web has List and Detail pages.
There is a list and 1 list has 10 items. When user scroll bottom, next page data will be loaded.
user click 40th item -> watch detail page (react-router) -> click back button
The main page scroll top of the page and get 1st page data again.
How to restore scroll position and datas without Ajax call?
When I used Vue js, i’ve used 'keep-alive' element.
Help me. Thank you :)
If you are working with react-router
Component can not be cached while going forward or back which lead to losing data and interaction while using Route
Component would be unmounted when Route was unmatched
After reading source code of Route we found that using children prop as a function could help to control rendering behavior.
Hiding instead of Removing would fix this issue.
I am already fixed it with my tools react-router-cache-route
Usage
Replace <Route> with <CacheRoute>
Replace <Switch> with <CacheSwitch>
If you want real <KeepAlive /> for React
I have my implementation react-activation
Online Demo
Usage
import KeepAlive, { AliveScope } from 'react-activation'
function App() {
const [show, setShow] = useState(true)
return (
<AliveScope>
<button onClick={() => setShow(show => !show)}>Toggle</button>
{show && (
<KeepAlive>
<Test />
</KeepAlive>
)}
</AliveScope>
)
}
The implementation principle is easy to say.
Because React will unload components that are in the intrinsic component hierarchy, we need to extract the components in <KeepAlive>, that is, their children props, and render them into a component that will not be unloaded.
Until now the awnser is no unfortunately. But there's a issue about it in React repository: https://github.com/facebook/react/issues/12039
keep-alive is really nice. Generally, if you want to preserve state, you look at using a Flux (Redux lib) design pattern to store your data in a global store. You can even add this to a single component use case and not use it anywhere else if you wish.
If you need to keep the component around you can look at hoisting the component up and adding a "display: none" style to the component there. This will preserve the Node and thus the component state along with it.
Worth noting also is the "key" field helps the React engine figure out what tree should be unmounted and what should be kept. If you have the same component and want to preserve its state across multiple usages, maintain the key value. Conversely, if you want to ensure an unmount, just change the key value.
While searching for the same, I found this library, which is said to be doing the same. Have not used though - https://www.npmjs.com/package/react-keep-alive

Resources