I want to add the total number of clicks using the increment field value but when I try to open the link I created, the total clicks do not increase at all.
did I do something wrong?
import {useParams} from 'react-router-dom';
import { useEffect, useState } from 'react';
import React from 'react';
import { firestore, app } from '../../firebase';
import { CircularProgress, Box, Typography } from '#mui/material';
function LinkRedirect() {
const {shortCode} = useParams();
const [loading, setLoading] = useState(true)
useEffect(() => {
const fethLinkDoc = async () => {
const linkDoc = await firestore.collection('links').doc(shortCode).get();
if (linkDoc.exists){
const{ longURL, linkID, userUid} = linkDoc.data();
firestore.collection('users').doc(userUid).collection('links').doc(linkID).update({
totalClicks: app.firestore.FieldValue.increment(1)
})
window.location.href = longURL;
} else {
setLoading(false)
}
}
fethLinkDoc()
}, [])
if (loading)
return (
<Box mt={10} textAlign="center">
<CircularProgress />
<Typography>Redirecting to the link</Typography>
</Box>
)
else return (
<Box mt={10} textAlign="center">
<Typography>Link is not valid</Typography>
</Box>
)
}
export default LinkRedirect
The problem is caused by these lines:
firestore.collection('users').doc(userUid).collection('links').doc(linkID).update({
totalClicks: app.firestore.FieldValue.increment(1)
})
window.location.href = longURL;
Here you queue the document update operation to increment the counter, then immediately navigate away from the page, cancelling the operation.
Because you are using an async function, you can simply add await before the call to update():
await firestore.collection('users').doc(userUid).collection('links').doc(linkID).update({
totalClicks: app.firestore.FieldValue.increment(1)
})
window.location.href = longURL;
However, I'd refactor your fethLinkDoc method to the following for readability:
const fethLinkDoc = async () => {
const linkDoc = await firestore.collection('links').doc(shortCode).get();
if (!linkDoc.exists) { // fail-fast
setLoading(false)
return
}
const { longURL, linkID, userUid } = linkDoc.data();
await firestore.collection('users')
.doc(userUid)
.collection('links')
.doc(linkID)
.update({
totalClicks: app.firestore.FieldValue.increment(1)
})
window.location.href = longURL;
}
Note: Your code currently doesn't properly handle when any of the firebase operations reject their promises. Either handle it manually or make use of an utility method like useAsyncEffect from a library.
Related
I have been building this mobile app with React Native/Expo and Firestore for a while now. The app schedules study sessions, and when a study session is active, a Pomodoro timer screen is to be shown, and when a session is inactive, the main homepage should be shown. However, I have been having trouble implementing this after a refactor to my database structure. Currently, for each schedule, a document is created in a subcollection corresponding to the user's UID. So, the path for a schedule would be "Users/(auth.currentUser.uid)/S-(auth.currentUser.uid)/(document id). To implement this feature, I have tried to run a function every second that checks through all of the documents and finds out whether a schedule is active, and if it is, it shows the Pomodoro timer screen. However, there is some weird behavior occurring. I am reading the database once using a Context Api, and the data shows perfectly in the screen where you view all your schedules, however in the function it is showing as an empty object. I have a feeling that it might be due to the bounds of the Context, however I am not sure. Does anyone know why?
CurrentDataProvider.js
import React, { createContext, useEffect, useState } from "react";
import {
doc,
getDocs,
onSnapshot,
collection,
query,
} from "firebase/firestore";
import { db, auth } from "../config/firebase";
export const CurrentDataContext = createContext({});
const CurrentDataProvider = ({ children }) => {
const [data, setData] = useState({});
useEffect(async () => {
if (auth.currentUser) {
const ref = query(
collection(
db,
"Users",
auth.currentUser.uid,
`S-${auth.currentUser.uid}`
)
);
const unsub = onSnapshot(ref, (querySnap) => {
let dat = {};
querySnap.forEach((doc) => {
dat[doc.id] = doc.data();
});
setData(dat);
});
return () => unsub;
}
}, []);
return (
<CurrentDataContext.Provider value={{ data, setData }}>
{children}
</CurrentDataContext.Provider>
);
};
export { CurrentDataProvider };
function being used to read schedules
const readSchedules = () => {
const currentTime = new Date();
Object.keys(data).forEach((key) => {
const clientSeconds =
currentTime.getHours() * 3600 + currentTime.getMinutes() * 60;
const startTimestamp = new Timestamp(data[key]["start"]["seconds"]);
const endTimestamp = new Timestamp(data[key]["end"].seconds);
const utcStartSeconds = startTimestamp.seconds;
const utcEndseconds = endTimestamp.seconds;
console.log(utcStartSeconds, clientSeconds, utcEndseconds);
const greaterTime = clientSeconds > utcStartSeconds;
const lessTime = clientSeconds < utcEndseconds;
const trueDay = data[key][dayOfWeekAsString(currentTime.getDay())];
if (trueDay) {
if (greaterTime && lessTime) {
setPomodoro(true);
setCurrentSchedule(key.toString());
console.log(`Schedule ${currentSchedule} selected!`);
return;
}
}
});
setPomodoro(false);
};
RootStack.js
import SplashScreen from "../screens/SplashScreen";
import AuthStack from "./AuthStack";
import React, { useState, useContext, useEffect } from "react";
import { View, ActivityIndicator } from "react-native";
import { auth } from "../config/firebase";
import { onAuthStateChanged } from "firebase/auth";
import { UserContext } from "./../components/UserProvider";
import { NavigationContainer } from "#react-navigation/native";
import FinalStack from "./MainStack";
import { CurrentDataProvider } from "../components/CurrentDataProvider";
const RootStack = () => {
const { user, setUser } = useContext(UserContext);
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
const authListener = auth.onAuthStateChanged(async (user) => {
try {
await (user ? setUser(user) : setUser(null));
setTimeout(() => {
setIsLoading(false);
}, 3000);
} catch (err) {
console.log(err);
}
});
return authListener;
}, []);
if (isLoading) {
return <SplashScreen />;
}
return (
<NavigationContainer>
{user ? (
<CurrentDataProvider>
<FinalStack />
</CurrentDataProvider>
) : (
<AuthStack />
)}
</NavigationContainer>
);
};
export default RootStack;
Thanks for all your help!
I'm trying to make a page to show the details of each video.
I fetched multiple video data from the back-end and stored them as global state.
This code works if I go to the page through the link inside the app. But If I reload or open the URL directory from the browser, It can not load the single video data.
How should I do to make this work?
Thanx
Single Video Page
import { useState, useEffect, useContext } from "react";
import { useParams } from "react-router-dom";
import { VideoContext } from "../context/videoContext";
const SingleVideo = () => {
let { slug } = useParams();
const [videos, setVideos] = useContext(VideoContext);
const [video, setVideo] = useState([]);
useEffect(() => {
const result = videos.find((videos) => {
return videos.uuid === slug;
});
setVideo((video) => result);
}, []);
return (
<>
<div>
<h1>{video.title}</h1>
<p>{video.content}</p>
<img src={video.thumbnail} alt="" />
</div>
</>
);
};
export default SingleVideo;
Context
import React, { useState, createContext, useEffect } from "react";
import Axios from "axios";
import { AxiosResponse } from "axios";
export const VideoContext = createContext();
export const VideoProvider = (props) => {
const [videos, setVideos] = useState([]);
const config = {
headers: { "Access-Control-Allow-Origin": "*" },
};
useEffect(() => {
//Fetch Vidoes
Axios.get(`http://localhost:5000/videos`, config)
.then((res: AxiosResponse) => {
setVideos(res.data);
})
.catch((err) => {
console.log(err);
});
}, []);
return (
<VideoContext.Provider value={[videos, setVideos]}>
{props.children}
</VideoContext.Provider>
);
};
I think the reason is because when you refresh the app, you fetch the video data on context and the useEffect on your single video page component runs before you receive those data.
To fix you can simply modify slightly your useEffect in your single video component to update whenever you receive those data:
useEffect(() => {
if (videos.length) {
const result = videos.find((videos) => {
return videos.uuid === slug;
});
setVideo((video) => result);
}
}, [videos]);
I'm attempting to run a function within the useEffect hook, so that on screen load it automatically calls the context and works out what to do.
But for whatever reason, the function just isn't firing. The screen loads successfully and renders, no errors, but just doesn't do anything.
Here's my component I'm calling the context from:
import React, { useContext, useEffect } from 'react';
import { View, StyleSheet, ActivityIndicator } from 'react-native';
import { AuthContext } from '../context/AuthContext';
const LoadingScreen = ({ navigation }) => {
const { userSignedIn } = useContext(AuthContext)
useEffect(() => {
userSignedIn()
}, [])
return (
<View style={styles.mainView}>
<ActivityIndicator style={styles.indicator} />
</View>
)
}
And my context file:
import React, { useState, useContext } from 'react';
import { navigate } from '../navigationRef';
import { Magic } from '#magic-sdk/react-native';
const m = new Magic('API key');
export const AuthContext = React.createContext();
export const AuthProvider = ({ children }) => {
const [user, setUser] = useState([]);
const userSignedIn = () => {
return async () => {
// Call Magic logged in
const loggedIn = await m.user.isLoggedIn();
console.log(loggedIn)
// If user logged in, save details to user, and redirect to dashboard
if (loggedIn === true) {
const { issuer, email } = await m.user.getMetaData();
console.log(issuer)
console.log(email)
setUser([issuer, email])
navigate('authorisedFlow')
// If user not logged in, redirect to login flow
} else {
console.log(userSignedIn)
console.log("Not signed in.")
navigate('loginFlow')
}
}
};
return (
<AuthContext.Provider value={{ user, userSignedIn }}>
{ children }
</AuthContext.Provider>
)
Can anyone point out what I'm doing wrong? Feels a simple one.. But can't figure it out.
You are returning an async function when calling userSignedIn so the following should work for you by making userSignedIn itself async to work for those await calls inside.
const userSignedIn = async () => {
// Call Magic logged in
const loggedIn = await m.user.isLoggedIn();
console.log(loggedIn)
// If user logged in, save details to user, and redirect to dashboard
if (loggedIn === true) {
const { issuer, email } = await m.user.getMetaData();
console.log(issuer)
console.log(email)
setUser([issuer, email])
navigate('authorisedFlow')
// If user not logged in, redirect to login flow
} else {
console.log(userSignedIn)
console.log("Not signed in.")
navigate('loginFlow')
}
};
I have been trying to use a cleanup function to cancel the API call I a user presses the back button before the request is resolved.
However I still receive the same error "Warning: Can't perform a React state update on an unmounted component.".
I am using fetch function, I added the abortController but still I receive the same warning.
import React, { useState, useEffect, useReducer, useContext } from "react";
import { ActivityIndicator } from "react-native";
import AllThumbnails from "../components/AllThumbnails";
import reducer from "../functions/reducer";
import { lightColors, darkColors } from "../constants/Colors";
import { ThemeContext } from "../context/context";
import ScreenContainer from "../components/UI/ScreenContainer";
export default function AllCatScreen(props) {
const { navigation, route } = props;
const [categories, setCategories] = useState([]);
const [state, dispatch] = useReducer(reducer, { catPage: 1 });
const [theme] = useContext(ThemeContext);
const { taxonomy } = route.params;
useEffect(() => {
const abortCtrl = new AbortController();
const opts = { signal: abortCtrl.signal };
let isActive = true;
fetch(`${siteURL}/wp-json/wp/v2/${taxonomy.endPoint}`, opts)
.then((response) => response.json())
.then((res) => {
if (isActive) {
setCategories([...categories, ...res]);
}
})
.catch((err) => console.log(err));
return function cleanup() {
isActive = false;
console.log(isActive);
abortCtrl.abort();
};
}, []);
if (categories.length == 0) {
return (
<ScreenContainer notYet={true}>
<ActivityIndicator size="large" color={theme.colors.text} />
</ScreenContainer>
);
} else {
return (
<ScreenContainer notYet={false}>
<AllThumbnails
data={categories}
navigation={navigation}
catThumb={true}
action={[state, dispatch]}
fetchData={fetchData}
/>
</ScreenContainer>
);
}
}
I have read that react native should support the AbortController. I am using Expo SDK 38 but even in the clean up function logging the console doesn't work. Does anyone know what's wrong?
I'm obviously not cleaning up correctly and cancelling the axios GET request the way I should be. On my local, I get a warning that says
Can't perform a React state update on an unmounted component. This is
a no-op, but it indicates a memory leak in your application. To fix,
cancel all subscriptions and asynchronous tasks in a useEffect cleanup
function.
On stackblitz, my code works, but for some reason I can't click the button to show the error. It just always shows the returned data.
https://codesandbox.io/s/8x5lzjmwl8
Please review my code and find my flaw.
useAxiosFetch.js
import {useState, useEffect} from 'react'
import axios from 'axios'
const useAxiosFetch = url => {
const [data, setData] = useState(null)
const [error, setError] = useState(null)
const [loading, setLoading] = useState(true)
let source = axios.CancelToken.source()
useEffect(() => {
try {
setLoading(true)
const promise = axios
.get(url, {
cancelToken: source.token,
})
.catch(function (thrown) {
if (axios.isCancel(thrown)) {
console.log(`request cancelled:${thrown.message}`)
} else {
console.log('another error happened')
}
})
.then(a => {
setData(a)
setLoading(false)
})
} catch (e) {
setData(null)
setError(e)
}
if (source) {
console.log('source defined')
} else {
console.log('source NOT defined')
}
return function () {
console.log('cleanup of useAxiosFetch called')
if (source) {
console.log('source in cleanup exists')
} else {
source.log('source in cleanup DOES NOT exist')
}
source.cancel('Cancelling in cleanup')
}
}, [])
return {data, loading, error}
}
export default useAxiosFetch
index.js
import React from 'react';
import useAxiosFetch from './useAxiosFetch1';
const index = () => {
const url = "http://www.fakeresponse.com/api/?sleep=5&data={%22Hello%22:%22World%22}";
const {data,loading} = useAxiosFetch(url);
if (loading) {
return (
<div>Loading...<br/>
<button onClick={() => {
window.location = "/awayfrom here";
}} >switch away</button>
</div>
);
} else {
return <div>{JSON.stringify(data)}xx</div>
}
};
export default index;
Here is the final code with everything working in case someone else comes back.
import {useState, useEffect} from "react";
import axios, {AxiosResponse} from "axios";
const useAxiosFetch = (url: string, timeout?: number) => {
const [data, setData] = useState<AxiosResponse | null>(null);
const [error, setError] = useState(false);
const [errorMessage, setErrorMessage] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
let unmounted = false;
let source = axios.CancelToken.source();
axios.get(url, {
cancelToken: source.token,
timeout: timeout
})
.then(a => {
if (!unmounted) {
// #ts-ignore
setData(a.data);
setLoading(false);
}
}).catch(function (e) {
if (!unmounted) {
setError(true);
setErrorMessage(e.message);
setLoading(false);
if (axios.isCancel(e)) {
console.log(`request cancelled:${e.message}`);
} else {
console.log("another error happened:" + e.message);
}
}
});
return function () {
unmounted = true;
source.cancel("Cancelling in cleanup");
};
}, [url, timeout]);
return {data, loading, error, errorMessage};
};
export default useAxiosFetch;
Based on Axios documentation cancelToken is deprecated and starting from v0.22.0 Axios supports AbortController to cancel requests in fetch API way:
//...
React.useEffect(() => {
const controller = new AbortController();
axios.get('/foo/bar', {
signal: controller.signal
}).then(function(response) {
//...
});
return () => {
controller.abort();
};
}, []);
//...
The issue in your case is that on a fast network the requests results in a response quickly and it doesn't allow you to click the button. On a throttled network which you can achieve via ChromeDevTools, you can visualise this behaviour correctly
Secondly, when you try to navigate away using window.location.href = 'away link' react doesn't have a change to trigger/execute the component cleanup and hence the cleanup function of useEffect won't be triggered.
Making use of Router works
import React from 'react'
import ReactDOM from 'react-dom'
import {BrowserRouter as Router, Switch, Route} from 'react-router-dom'
import useAxiosFetch from './useAxiosFetch'
function App(props) {
const url = 'https://www.siliconvalley-codecamp.com/rest/session/arrayonly'
const {data, loading} = useAxiosFetch(url)
// setTimeout(() => {
// window.location.href = 'https://www.google.com/';
// }, 1000)
if (loading) {
return (
<div>
Loading...
<br />
<button
onClick={() => {
props.history.push('/home')
}}
>
switch away
</button>
</div>
)
} else {
return <div>{JSON.stringify(data)}</div>
}
}
ReactDOM.render(
<Router>
<Switch>
<Route path="/home" render={() => <div>Hello</div>} />
<Route path="/" component={App} />
</Switch>
</Router>,
document.getElementById('root'),
)
You can check the demo working correctly on a slow network
Fully cancellable routines example, where you don't need any CancelToken at all (Play with it here):
import React, { useState } from "react";
import { useAsyncEffect, E_REASON_UNMOUNTED } from "use-async-effect2";
import { CanceledError } from "c-promise2";
import cpAxios from "cp-axios"; // cancellable axios wrapper
export default function TestComponent(props) {
const [text, setText] = useState("");
const cancel = useAsyncEffect(
function* () {
console.log("mount");
this.timeout(props.timeout);
try {
setText("fetching...");
const response = yield cpAxios(props.url);
setText(`Success: ${JSON.stringify(response.data)}`);
} catch (err) {
CanceledError.rethrow(err, E_REASON_UNMOUNTED); //passthrough
setText(`Failed: ${err}`);
}
return () => {
console.log("unmount");
};
},
[props.url]
);
return (
<div className="component">
<div className="caption">useAsyncEffect demo:</div>
<div>{text}</div>
<button onClick={cancel}>Abort</button>
</div>
);
}
This is how I do it, I think it is much simpler than the other answers here:
import React, { Component } from "react";
import axios from "axios";
export class Example extends Component {
_isMounted = false;
componentDidMount() {
this._isMounted = true;
axios.get("/data").then((res) => {
if (this._isMounted && res.status === 200) {
// Do what you need to do
}
});
}
componentWillUnmount() {
this._isMounted = false;
}
render() {
return <div></div>;
}
}
export default Example;