React - update remote data, update the view - reactjs

Let's say I fetch some data. Then, I make some modifications to the data - for example, I click 'delete' on a single record, and an appropriate request is made to update the remote database.
Now, what's the most popular way to keep local state (the view) synchronized? Should I just delete the record from the local state, and hope the DB indeed got updated and everything's in sync? Or perhaps I should instantly make yet another request and fetch the entirety of the updated data and populate the view?

What i suggest is that you assign the original state to a variable and then you make the call to the database and update the state by deleting the entry and then you check the response from the db if it is ok so you keep the new state, if not you re-set the state with the original that you stored on the variable and show an error message, this is a basic example using axios
import React, {useState, useEffect} from 'react';
import axios from 'axios';
import { notification } from 'antd';
export default const myCommponent = () => {
const [items, setItems] = useState([])
const [fetchingData, setFetchingData] = useState(true)
// make the call to the db to get the items
useEffect(() => {
// here you create the function that will fetch the data from the api once the component is mounted
async function geData() {
await axios({
url: 'your_url',
method: 'GET'
}).then((response) => {
setItems(response.data)
setFetchingData(false)
});
});
}
if (fetchData) {
// if the fetchingData is true so you send the request this to prevent your component from changing the state multiple times
geData();
}
}, []);
const handleDelete = (e) => {
// here you imutate the state
const originalList = [ ...items ];
// you delete the item from the imutated state
const newList = originalList.filter((item) => {
return item.key !== e.key;
});
// you set the new state
setItems(newList);
// you send your request to the api to delete the desired item
axios({
url: `delete url/`,
method: 'DELETE'
}).then((response) => {
// you check the response sent from the api
if (response.data.status_code === 200) {
// if it OK so you just have to show a confirmation message
notification['success']({
message: response.data.message
});
} else {
// if not yuou show an error message
notification['error']({
message: response.data.message
});
// you reset the state to the original with the deleted item.
setItems(originalList);
}
});
};
render(){
return(
<div>
// return the component here
</div>
);
}
}
I hope that will give you the desired result.

There are a few ways this can be done.
Sync
Make the network call to delete the item
Wait for its reponse
Show a loading/wait state meanwhile
Once you have received the response, update the view accordingly i.e. if it's an error, tell the user that
Async
Make the network call to delete the item
Update the view as soon as the network call is made but save the initial values
Once you receive the response if there is an error, you need to add back that item to your view
If it's a success, you need to do nothing
The advantage of 2 is that you provide the feedback to the user immediately and it gives the impression that your app is fast no matter how slow their internet is. You are handling everything in the background. I believe it can be bad for sensitive things that you NEED to be updated and the user may not see or get back to those places once they have done an action there.
So, doing stuff like simply archiving a conversation can be handled by 2nd method.
Doing stuff like blocking a user should be handled by 1st method because it can be sensitive to the user. If you do handle it by 2nd you need to convey it clearly to the user that the action has failed if it fails.

Related

How to show loading state only if data is not received yet. Next.js ssr

I have multiple getServerSideProps in my project and I have a header which displays pages and I have to wait for a page to be opened once I click upon it since I need data to be fetched. Once they are fetched the page will be open.
One approach I used to show user a loading state is to use routeChangeStart BUT I stumbled upon one problem and so I would like not to use this case.
If I go on a page and the data is fetching I want to show user a spinner or some indicator and once the data is fetched I want to stop the indicator/spinner.
As you probably figured out, getServerSideProps runs on the server and is blocking. The fetch request needs to complete before the HTML is sent to the user (i.e., the page is changed). So if you want to show a loading indicator, you need to move that fetch request to the client.
For instance, if you probably have a page with this basic structure:
export default function Page({ data }) {
return <div>{data.name}</div>
}
export async function getServerSideProps() {
const response = await fetch('https://example.com/api')
const data = await response.json()
return {
props: { data },
}
}
const fetcher = url => fetch(url).then(res => res.json());
export default function Page() {
const { data } = useSWR('https://example.com/api', fetcher)
if (!data) return <LoadingSpinner />
return <div>{data.name}</div>
}
Or if you don't need SWR and can use a simple fetch request:
export default function Page() {
const [data, setData] = useState()
useEffect(() => {
fetch('https://example.com/api')
.then(async(response) => {
const json = await response.json()
setData(json)
})
})
if (!data) return <LoadingSpinner />
return <div>{data.name}</div>
}
P.S. If the initial fetch request in getServerSideProps used sensitive information (e.g., API secret credentials), then go ahead and setup a Next.js API route to handle the sensitive part and then fetch the new route.
I just used routeChangeStart.
I didn't want to use it since router.push('/map') didn't work in pages/index.tsx file but I solved this issue by creating a new component putting router.push in useeffect and rendering a loader.
routeChangeStart was in _app.js and because of this in index.js router.push() didn't work - I tested it
routeChangeStart - how it works?
When we click on a page the data is being fetched on the server and the page will only be displayed to us once the data is fetched. So we can make the next thing, we can just intercept the route change.
When we click on a link(we wait for data to fetch) we set loading state in routeChangeStart to true and if we moved to another page(it means we fetched the data) we invoke routeChangeComplete which runs once we moved to the route we wanted to, and here we set loading state to false. And after this I just pass the loading state using React Context

React multiple http calls when navigating the application using the URL

I have a component which has filtering, searching and pagination capabilities. What I'm trying is to create a queryString and attach to the URL such that I can later copy and paste it in another browser so that I can reuse the filters.
To extract the query params from the URL I'm using the useLocation hook
const useQuery = () => new URLSearchParams(useLocation().search);
const pageNo = useQuery().get('page') ?? 1;
I'm using the useEffect hook to track for changes of the page query parameter value, and dispatch an action which will update the pageNo in the state object of my reducer.
React.useEffect(() => {
dispatch({
type: actionDescriptor.CHANGE_PAGE,
payload: pageNo
});
}, [pageNo]);
I have another useEffect hook which handles the fetch of the data, and gets triggered when the pageNo changes. I'm using the useNavigate to create and navigate to the new location if the http call was successful
const nav = useNavigate();
React.useEffect(() => {
(async function get() {
const response = // make http call and get response
if (response.status === 200) {
dispatch({
type: actionDescriptor.FETCH_SUCCESS,
payload: {
data: response.data['value'],
}
});
nav (`/data?page=${state.pageNo}`);
}
/// handle error
}
})();
}, [state.pageNo, state.pageSize, state.filter]);
When I'm navigating using the UI, selecting a different page for example, all works well, there is a single http call, the browser url is updated as expected (e.g. localhost/mydata?page=2). If however I'm copying the url and paste it in another window, it makes two http calls, and renders the dom twice. What might be the cause for this?
my guess is due to the parameters you are listening on state.pageNo, state.pageSize, state.filter. I'm assuming all of these are null/empty at the beginning of your app. Once you copied and pasted the url, two of these parameters will change which will cause the useEffect to be called twice.
put in a console.log in the useEffect to confirm that. Once that's confirmed, I would re-examine the list of parameters to see if you need to listen to all of them.
I would take a look at the pageNo. It looks like it might be changing from default value to 2 since you have 2 useEffects probably firing for the same render.

React-query: how to update the cache?

I have a very basic app that fetches a user and allows to change his name. I fetch the user with React query so I can benefit from the cache feature. It works.
However, when I want to update the user, I use a classic post request with axios. Once the user is updated in the database, I need to update the cache directly in the updateUser() function. I've seen tutorials on the web that use queryCache.setCache, but it doesn't work here. How to fix this? Or maybe there is a better way to handle such queries?
Also, I notice a huge number of rendering... (see the "user render" console.log in the app file).
For the convenience, I've made a small script on a codesandbox with the pokeapi:
https://codesandbox.io/s/api-service-syxl8?file=/src/App.js:295-306
Any help would be greatly appreciated!
So, I'll show you what I do:
const updateUser = async (userUpdates: User) => {
const data = await UserService.updateUser(userUpdates); // return axios data
return data;
}
// if you want optimistic updating:
const { mutate: mutateUser } = useMutation(updateUser, {
onMutate: async (userUpdates) => {
// Cancel any outgoing refetches (so they don't overwrite our optimistic update)
await queryClient.cancelQueries(['user', userUpdates.id]);
// Snapshot the previous value
const previousUser = queryClient.getQueryData(['user', userUpdates.id]);
// Optimistically update to the new value
queryClient.setQueryData(['user', userUpdates.id], userUpdates);
// Return a context with the previous user and updated user
return { previousUser, userUpdates }; // context
},
// If the mutation fails, use the context we returned above
onError: (err, userUpdates, context) => {
queryClient.setQueryData(['user', context.userUpdates.id], context.previousUser);
},
// Always refetch after error or success:
onSettled: (userUpdates) => {
queryClient.invalidateQueries(['user', userUpdates.id]);
}
});
// then to update the user
const handleUpdateUser = (userUpdates: User) => mutateUser(userUpdates);
This all comes from the docs:
Optimistic Updates

Update list after request react hooks

I have a list of items stored in state. Upon form submission i add another item to the list and then save that as the new state. This newly added item has the status "pending." At the same time i send a post request and if the post request fails i want to update that particular items status to "error". The problem is, the state isn't updated by the time the request fails, and so i'm trying to update a state that isn't set.
I am using react hooks, so one possibility is to call the request only after the state has updated:
useEffect = (()=>{
function getRequest(URL, id, freq) {
request happens here
}
}),[state])
Previously, before putting the getRequest function in useEffect, it was called by another function in a child component.
My question consists of several parts:
1) how do i get the parameters URL, id, freq into the useEffect function?
2) i don't want to run the getRequest function on first render, so how can i negate this?
3) is my general pattern of doing things here good (i'm sure it shouldn't be this difficult).
If you want to make sure you're updating an item that was already added to the state, then indeed the way to make this synchronously is to use useEffect.
However, I added an extra state variable, which represents the amount of items.
Then, whenever a new item is added, the API request will be executed and the (existing) item will be updated accordingly:
const [itemsAmount, setItemsAmount] = setState(0);
const [items, setItems] = setState([]);
function addItem(itemData) {
const newItem = {
...itemData,
status: 'pending',
}
setItems([...items, newItem]);
setItemsAmount(itemsAmount + 1);
};
useEffect(async () => {
try {
// Execute the API call
const response = await apiCall();
if (resposne && response.data) {
// Add the successfully updated version of the item
newItem.status = 'done';
newItems.data = response.data;
setItems([...items, newItem]);
}
} catch (err) {
// If the API call fails, add the "failed" version of the item
newItem.status = 'failed';
setItems([...items, newItem]);
}
}, [itemsAmount]);

How to fetch data each time user opens the screen and update the state only if the fetched data is different than the current data in state

I am relatively new to react, react native and redux and I have one question about fetching the data from the api and updating the state.
What I want to do is:
- each time a user opens the screen (i.e Posts) I want to fetch data from the api and update the state.
So that:
- I always have newly fetched data in the state and rendered on the screen.
If I fetch the data only in componentDidMount lifecycle method then I won't always have fresh data.
Example:
There are two screens Post and Posts. If a user opens posts screen for the first time then componentDidMount is triggered and posts are fetched and rendered. Then he goes to Post screen adds a new post and goes back to Posts screen then he will not see that new post because the posts will be displayed from the state fetched in componentDidMount method.
What is the best way to handle this and always have latest data?
class Posts extends Component {
componentDidMount() {
this.props.fetchPosts();
}
componentDidUpdate(prevProps) {
this.props.fetchPosts();
}
render () { ... }
}
const mapStateToProps = state => {
return {
posts: state.posts.posts,
loading: state.posts.loading,
}
}
const mapDispatchToProps = dispatch => {
return {
fetchPosts: () => dispatch(actions.fetchPosts()),
}
}
export default connect(mapStateToProps, mapDispatchToProps)(Posts)
Action
export const fetchPosts = () => {
return dispatch => {
dispatch(fetchStarted());
apiFetchPosts()
.then(({data}) => {
const posts = data.data;
dispatch(fetched(posts))
})
.catch(error => {
dispatch(fetchFailed(error));
})
}
}
const fetchStarted = () => {
return {
type: actionTypes.POSTS_FETCH_START,
}
}
const fetched = (posts) => {
return {
type: actionTypes.POSTS_FETCH_SUCCESS,
posts,
}
}
const fetchFailed = (error) => {
return {
type: actionTypes.POSTS_FETCH_FAILED,
}
}
Update as you go
In your case, you should be sending a POST request to insert a new post. That request should have the newly created post as a response. You should use that response to update the list of posts you have in the redux state. Simply add the new post to the list.
The same goes for removing posts, once you send a DELETE request, you should also remove that post from the state.
Add timed cache
If you want to refetch the posts when you haven't fetched it for a while, you should implement some kind of caching solution:
When you fetch the posts for the first time you can also store the timestamp of the request. When the user visits the Posts page, you can check if the timestamp is too old e.g. 5 minutes in the past, and refetch it accordingly.
Refetch everytime
If you want to simply refetch the posts every time the user visits that page, you can use the useEffect hook or a combination of componentDidMount and componentDidUpdate.
Please note:
You may call setState() immediately in componentDidUpdate() but note that it must be wrapped in a condition like in the example above, or you’ll cause an infinite loop.
Source
You could use componentDidUpdate

Resources