I am writing simple blog collection. I have newest react-router and react-redux. I have all blogs in my store. When I navigate to single blog page it works fine at first time, but when I refresh browser or enter dynamic url straight to address bar, component loose data. What kind of hook I should use to fetch data? I have only this in my SingleBlog to fetch data:
const id = useParams().id
const blog = useSelector(state => state.blogs.find(b => b.id === id))
When you refresh the browser, your whole app restarts, that includes your state that holds the data.
If you want to keep the data even after the browser refreshes, you should then save your data in the localStorage and modify the code so that it gets the state from the data saved in the localStorage.
How you might do that:
When you fetch the data
...
// Where data is the variable that holds your fetched data
localStorage.setItem('data_array', JSON.stringify(data));
In the blog reducer
...
// Instead of having the initial state as empty array `[]`, you make it equal
// to whichever data is stored in the localStorage. Because if the localStorage
// have no data, it will still return an empty array `[]`
const initialState = JSON.parse(localStorage.getItem('data_array'));
export function blogReducer(state = initialState, action) {
switch (action.type)
...
}
In the blog page
...
// You keep blog const as it is since we took care of the localStorage in
// the reducer
const blog = useSelector(state => state.blogs.find(b => b.id === id))
...
Quick note:
You shouldn't rely on refreshing the browser to fetch a new blog post/data. You could, however, use setInterval() to send a fetch request every x time to see if there are any new posts/data, Or you could add a refresh button to your app that fires a request to get any new data
Related
In my project I use ReactJS in combination with redux and firebase.
Creating a thunk to make async calls to firebase and store the data in redux.
When I'm fetching my files from firebase storage.
Using this method:
try {
let list = [];
await storage
.ref()
.child(path)
.listAll()
.then((res) => {
res.items.forEach((item) => {
storage
.ref()
.child(item.fullPath)
.getDownloadURL()
.then((urlRes) => {
list.push({
name: item.name,
url: urlRes,
});
});
});
});
dispatch(getFileActionSuccess(list));
This method works as intended.
It returns an array of files with their url to view/download them.
The problem is when I try to access this object in my state, it returns an empty array.
Even though when checking using Redux Devtools, I can clearly see that after the list was dispatched. And I could see the correct data.
Devtools image
Note: this is not the real code but a representation
function page() {
getFiles();
<filesList/>
}
function filesList() {
const files = useSelector((state) => state.files, _.isEqual);
console.log(files);
return (..insert render..);
}
But when logging the files. It shows an empty array at first. But when expanding it, it shows the correct data. But it doesn't render it. As I don't understand why it isn't showing like it is supposed to I no longer know what to do and how to fix this.
Simply fetch the data on component mount and component update, and update your state accordingly.
If you’re using React Hooks, you can use React.useState() and give it a dependency. In this case the dependency would be the part of your state which will update upon completion of your HTTP request.
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
I use React with Redux and Firebase. Here is one of the functions from my Action.js
export const loadItemsInCategory = (categoryId) => {
return (dispatch) => {
let itemsArray = [];
firestoreService.getItemsInCategory(categoryId)
.then(updatedGroceryList => {
itemsArray = updatedGroceryList;
console.log(`category id is ${categoryId}`)
dispatch(loadItemsInCategoryHelper(categoryId, itemsArray))
})
.catch((error) => console.log(error));
}
}
It's a normal FireStore query. Here is what happens in firestoreService.getItemsInCategory(categoryId)
export const getItemsInCategory = async (categoryId) => {
console.log(`firebase category id is ${categoryId}`)
const snapshot = await db.collection('Item').where('category', '==', categoryId).get()
return snapshot.docs.map(doc => {console.log("called");return {id: doc.id, ...doc.data()}});
}
Right now, my application shows the list of items in the given Category. However, the list does not get updated when a new Item is added to the category by someone else. In other words, additions in FireStore collection does not reflect on my screen unless I refresh the page.
How can I code my webapp in such a way that any change on the FireStore end gets reflected on my webapp?
Thanks in advance!
Your code is doing a one-time query with get(). Queries made like this are not realtime. They don't refresh.
If you want to receive updates to your query in realtime, you should follow the documentation for realtime queries. Instead of using get(), you will use onSnapshot(). And instead of getting a promise, you will attach a listener callback that will be invoked whenever there is a change in the results of the query. Because of these differences, your code will look drastically different.
const { categoryId } = useParams();
const { clinicsById, clinicsList, loading } = useSelector(
(state) => state.clinics
);
const clinics = clinicsList.map((clinicId) => clinicsById[clinicId]);
useEffect(() => {
dispatch(fetchClinicsForCategory({ categoryId }));
}, [categoryId, dispatch]);
return <div>{clinics}</div>;
Basically, this component displays different data depending on the categoryId.
But after the first render, consequent renders to different categoryId are showing the previous state.
In this scenario, I believe the state from the previous render displays without waiting for the new state to arrive. FetchClinicsForCategory() makes an API call to grab the new data. How can I always show the latest data? When I print the clinics to the console, I can see that the new data arrives.
https://streamable.com/5m5o6u
This is the reducer code. My action is making a call to an API and getting the data and passing it to this reducer.
getClinicsSuccess(state, action) {
const clinics = action.payload;
state.loading = false;
state.clinicsList = clinics.map((clinic) => clinic.clinicId);
clinics.forEach((clinic) => {
state.clinicsById[clinic.clinicId] = clinic;
});
}
Edit: Added a recording of the issue. You can see that even though I click on a category with 1 service, it shows the multiple services from the previous time I rendered the page.
Edit: Added reducer code.
So it wasn't an issue with Redux...
I had a separate state keeping a list of filtered clinics and that was the list being rendered instead of my actual new data. I'll need to rework the logic. Thanks for the suggestions!
I currently have an application using redux and I have it set that every time the app loads it checks the JWT token is valid and adds the user data to the state.
I was wondering what the differences are between calling the api and then storing data in the state every reload or storing the data once in localStorage?
How the code is setup with calling the api and storing with redux.
CHECK TOKEN
const token = localStorage.UserIdToken;
if (token) {
const decodedToken = jwtDecode(token);
if (decodedToken.exp * 1000 < Date.now()) {
store.dispatch(logoutUser());
} else {
store.dispatch({ type: SET_AUTHENTICATED });
axios.defaults.headers.common['Authorization'] = token;
store.dispatch(getUserData());
}
}
getUserData()
export const getUserData = () => async dispatch => {
try {
const res = await axios.get('/user');
dispatch({
type: SET_USER,
payload: res.data,
});
}
...
};
First of all, storing the data once in localStorage means that the user will not receive updates to his data, but will always receive the same. Think of seeing the same social media feed every time you log in, which would be the case if it would be saved in localStorage and not requested from the api every reload.
Second, storing data in localStorage instead of the redux state means you use the benefits of using state and redux - redux ensures that components will rerender when state they depend on changes. This ensures that components are responsive to user actions. localStorage won't do that.
Following your comment, I think there is another reason you should consider:
Using localStorage might pose problems if you want to change the user data (add a field for instance). If the data was in 1 place, you could change all user data and let users pull the new data on the next reload. If the data is in localStorage, you will need to add code to your app that will change the existing data on first reload, and then do nothing on other times. This is not a good pattern, and has a better chance of having bugs and problems.