Conditional fetching with Redux-Toolkit RTK-Query - reactjs

I'm trying to migrate from createAsuncThunks to RTK-Query. On page load I check if there's no user in store then fetch user and other data from backend.
This code works well with createAsuncThunks but how it should like with RTK-Query?
const InitApp = () => {
const dispatch = useAppDispatch();
const user = useAppSelector((state) => state.user);
const ignore = useRef(false);
useEffect(() => {
if (!user && !ignore.current) {
Promise.all([
dispatch(getUser()),
dispatch(getRawMedias()),
dispatch(getFaces()),
dispatch(getProjects()),
]);
}
return () => {
ignore.current = true;
};
}, [dispatch, user]);
if (!user) {
return <Loader areaLoader />;
}
return <Outlet />;
};
I think this approach is wrong but it's not clear how to make it properly:
const {data} = useGetUserQuery()
useEffect(() => {
if (!data) {
// can't call other hooks here
}
}, [data])

Hi you can pass skip parameter, and combine it with initalState like so:
const user = useAppSelector((state) => state.user);
const { data = user } = useUserQuery(undefined, { skip: Boolean(user) });
now this would trigger only if there is no user, and data would be equal to user, if there is user

Related

How to immediately update a state in React

I'm struggling with this problem and I've already tried many solutions but none of them fit me.
I have a context that I use to share information that I get from an API. I will summarize the files for you:
file: useGetInfo.tsx
type InfoContextData = { ... }
type Props = { ... }
type InfoResponseProps = { ... }
export const InfoContext = createContext<InfoContextData>({} as InfoContextData)
export const InformationProvider = ({ children }: Props) => {
const isBrowser = typeof window !== `undefined`
const [infoStorage, setInfoStorage] = useState(
isBrowser && localStorage.getItem('info')
? String(localStorage.getItem('info'))
: undefined
)
const [result, setResult] = useState<InfoResponseProps | null>(null)
const getInfo = useCallback(async (value: string) => {
const url = `<URL_FROM_API${value}>`
await axios.get(url)
.then((response) => {
setResult(response.data)
})
.catch((_) => {
setResult(null)
})
})
useEffect(() => {
if (!infoStorage) {
return
}
getInfo(infoStorage)
}, [infoStorage, getInfo])
return (
<InfoContext.Provider
value={{
result,
setResult,
infoStorage,
setInfoStorage,
getInfo,
}}
>
{children}
</InfoContext.Provider>
)
}
Then in the component I call the context:
file: SomeComponent.tsx
const Component = () => {
const { setInfoStorage, getInfo, result } = useContext(InfoContext)
const [input, setInput] = useState('')
const handleInfoSubmit = useCallback(() => {
getInfo(input)
if (!result || !result?.ok) {
localStorage.removeItem('info')
setInfoStorage(undefined)
}
setInfoStorage(input)
localStorage.setItem('info', 'input')
setInput('')
}, [input, result, getInfo, setInfoStorage, setInput])
return (
...
<Form onSubmit={handleInfoSubmit}>
<input>
...
</Form>
)
}
Basically, the user inserts a code in the form and when he submits the form, it runs the handleInfoSubmit function. Then, the code runs the function getInfo() and after requesting the API it returns the information to the state result.
The problem is in the SomeComponent.tsx file: when I run the function getInfo(input) I need the information in the state result but at the time axios finishes the request to the API and the code goes to the if (!result || !result?.ok) line, the result state is not still fulfilled.
I know that React/Gatsby can't update immediately the state like what I need, but is there a way to overcome this problem? Thanks in advance.
I think the value of the result would always be stale inside the handleInfoSubmit function per your code.
Rewrite the getInfo and handleInfoSubmit like this
// Return data from getInfo so that we can use the value directly in handleInfoSubmit
const getInfo = useCallback(async (value: string) => {
const url = `<URL_FROM_API${value}>`
try {
const { data } = await axios.get(url);
setResult(data)
return data;
} catch {
setResult(null)
}
return null;
})
const handleInfoSubmit = useCallback(async () => {
// await getInfo and get the axios response data.
const result = await getInfo(input)
if (!result || !result?.ok) {
localStorage.removeItem('info')
setInfoStorage(undefined)
}
setInfoStorage(input)
localStorage.setItem('info', 'input')
setInput('')
}, [input, getInfo, setInfoStorage, setInput])

How to use useNavigate outside react hook?

Gets list of emails from firestore and checks if current user is registered and then redirects them to sign up if they are new user.
The code is functional(it redirects succesfully) but get the following error:
arning: Cannot update a component (BrowserRouter) while rendering a different component You should call navigate() in a React.useEffect(), not when your component is first rendered.
const navigate = useNavigate();
let hasEmail = false;
const [emailList, setEmailList] = useState([]);
const emailRef = collection(db, "emails");
useEffect(() => {
const getEmails = async () => {
const data = await getDocs(emailRef);
setEmailList(
data.docs.map((doc) => ({
...doc.data(),
}))
);
};
getEmails();
}, []);
const emailCheck = (emails) => { //checks if email exists
hasEmail = emails.some((e) => e.email === auth.currentUser.email);
};
const direct = () => { // redirects to required page
if (hasEmail) {
navigate("/index");
} else {
navigate("/enterdetails");
}
};
emailCheck(emailList);
direct();
Move the email checking logic into a useEffect hook with a dependency on the emailList state.
const navigate = useNavigate();
const [emailList, setEmailList] = useState([]);
const emailRef = collection(db, "emails");
useEffect(() => {
const getEmails = async () => {
const data = await getDocs(emailRef);
setEmailList(
data.docs.map((doc) => ({
...doc.data(),
}))
);
};
getEmails();
}, []);
useEffect(() => {
if (emailList.length) {
const hasEmail = emailList.some((e) => e.email === auth.currentUser.email);
navigate(hasEmail ? "/index" : "/enterdetails");
}
}, [auth, emailList, navigate]);
This might not run without the proper firebase config but check it out
https://codesandbox.io/s/elated-bell-kopbmp?file=/src/App.js
Things to note:
Use useMemo for hasEmail instead of emailCheck. This will re-run only when emailList changes
const hasEmail = useMemo(() => {
//checks if email exists
return emailList.some((e) => e.email === auth.currentUser.email);
}, [emailList]);
There isn't really a point in having this in a react component if you are just redirecting away. Consider having the content of 'index' at the return (</>) part of this component. Only redirect if they aren't authorized
useEffect(() => {
if (!hasEmail) {
navigate("/enterdetails");
}
//else {
// navigate("/index");
//}
}, [hasEmail, navigate]);

UseEffect is not called

I have a question about useEffect. My useEffect is not fetching the data the first time, I have to switch route for it to have the data I needed
const Comments = ({ ...rest }) => {
const theme = useTheme();
const classes = useStyles({ theme });
const [users, setUsers] = useState([]);
const { push } = useHistory();
const { token, loading } = useContext(AuthContext)
const dispatch = useDispatch();
const allUsers = useSelector(state => state.allUsers);
const comments = useSelector(state => state.listCommentsByBookId);
const listBooks = useSelector((state) => state.userListBooks);
const isFetching = useSelector((state) => state.isFetching);
const [stateReady, setReadyForRender] = useState(false)
const redirectTo = ( rowData ) => {
push({
pathname: ROUTE.USERS_DETAILS,
user: rowData
});
}
const options = {
filterType: 'checkbox',
selectableRowsHeader: false,
selectableRowsHideCheckboxes: false,
selectableRowsOnClick: false,
onRowClick: redirectTo,
};
const getAllComments = async () => {
var allusersId = [];
//get all ids
await allUsers.map((user) => {
allusersId.push(user.uid);
})
//get all books from users
await allusersId.map(async (id) => {
await dispatch(getUserListBooks(apiURL + `api/bdd/userListBooks/${id}`, token))
})
var listArray = [];
//filter the array and delete empty rows
listArray.push(listBooks);
var newArray = listArray.filter(e => e);
//map every user and stock the list of books in string
await newArray.forEach(async (book)=> {
await book.map(async (book) => {
await dispatch(getCommentsByBookId(apiURL + `api/bdd/ratingByBook/${book.id}`, token));
})
})
setReadyForRender(true)
}
useEffect(() => {
console.log('is fetching', isFetching)
if(comments.length === 0) {
getAllComments();
}
}, [stateReady])
console.log('COM', comments);
return (
<div>
{stateReady &&
<Card>
<Box className={classes.tableContainer} sx={{ minWidth: 1050 }}>
<MUIDataTable
data={comments}
columns={columns}
options={options}
/>
</Box>
</Card>}
</div>
);
};
Why? It might be related to async await but I'm stuck here.
If you want to fetch these informations on the first render, you'll have to pass an empty array as the second parameter of your useEffect.
The reason your useEffect is not called is because stateReady does not change during the course of your current code.
See this link, particularly the note section, it explains way better than me how the empty array as second parameter works.
Can you replace the useEffect section to the below code:
useEffect(() => {
(async () => {
console.log('is fetching', isFetching)
if(comments.length === 0) {
getAllComments();
}
})()
}, [stateReady])
You can read more about this in this link
You can use eslint to show errors when coding with hooks. In this case if you want useEffect to handle stateReady, please provide it in the function getAllComments() => getAllComments(stateReady) and when you call this function in useEffect with [stateReady] as dependencies, it'll work.
You should remove stateReady from your dependency array in the useEffect hook. Adding variables in the dependency array means that the use Effect hooks fires only when one of the dependencies changes. Here's how to use useEffect as lifecycle methods https://reactjs.org/docs/hooks-effect.html
useEffect(() => {
console.log('is fetching', isFetching)
if(comments.length === 0) {
getAllComments();
}
});

Firebase + react : read document in auth state changed and add it to context

Based on https://dev.to/bmcmahen/using-firebase-with-react-hooks-21ap I have a authentication hook to get user state and firestore hook to get user data.
export const useAuth = () => {
const [state, setState] = React.useState(() => { const user = firebase.auth().currentUser return { initializing: !user, user, } })
function onChange(user) {
setState({ initializing: false, user })
}
React.useEffect(() => {
// listen for auth state changes
const unsubscribe = firebase.auth().onAuthStateChanged(onChange)
// unsubscribe to the listener when unmounting
return () => unsubscribe()
}, [])
return state
}
function useIngredients(id) {
const [error, setError] = React.useState(false)
const [loading, setLoading] = React.useState(true)
const [ingredients, setIngredients] = React.useState([])
useEffect(
() => {
const unsubscribe = firebase
.firestore()
.collection('recipes')
.doc(id)
.collection('ingredients') .onSnapshot( snapshot => { const ingredients = [] snapshot.forEach(doc => { ingredients.push(doc) }) setLoading(false) setIngredients(ingredients) }, err => { setError(err) } )
return () => unsubscribe()
},
[id]
)
return {
error,
loading,
ingredients,
}
}
Now in my app I can use this to get user state and data
function App() {
const { initializing, user } = useAuth()
const [error,loading,ingredients,] = useIngredients(user.uid);
if (initializing) {
return <div>Loading</div>
}
return (
<userContext.Provider value={{ user }}> <UserProfile /> </userContext.Provider> )
}
Since UID is null before auth state change trigger, firebase hook is getting called with empty key.
How to fetch data in this scenario once we understand that user is logged in.
May be you can add your document read inside auth hook.
export const useAuth = () => {
const [userContext, setUserContext] = useState<UserContext>(() => {
const context: UserContext = {
isAuthenticated: false,
isInitialized: false,
user: auth.currentUser,
userDetails: undefined
};
return context;
})
function onChange (user: firebase.User | null) {
if (user) {
db.collection('CollectionName').doc(user.uid)
.get()
.then(function (doc) {
//set it to context
})
});
}
}
useEffect(() => {
const unsubscribe = auth.onAuthStateChanged(onChange)
return () => unsubscribe()
}, [])
return userContextState
}
You can use some loading spinner in your provider to wait for things to complete.

What's the best way to do request multiple data from Firebase in React

I was wondering what would be the best way to get multiple data from Firebase in an async function to wait for some data from the first request. I'm using this code right now but it's not reliable and it breaks sometimes saying that it can't fetch the data for the second call as it's undefined.
function useOccasion() {
const [occasionData, setOccasionData] = useState(null)
const [friend, setFriend] = useState(null)
let { occasion } = useParams()
useEffect(() => {
const unsubscribe = firestore.collection('occasions').doc(occasion)
.onSnapshot(async eventData => {
setOccasionData({id: eventData.id, ...eventData.data()})
let friendData = await firestore.collection("friends").doc(eventData.data().friend).get();
setFriend({id: friendData.id, ...friendData.data()});
})
return () => unsubscribe()
}, [occasion])
return [occasionData, friend]
}
If there's a more robust way to achieve this that would be amazing.
I'd suggest breaking your data fetches into two hooks, one for each collection occasion and friends.
I'm not sure how you've set firebase up, but I access it through context.
e.g.
const useOccasion = () => {
const firebase = useContext(FirebaseContext)
const [occasions, setOccasions] = useState([])
const [loading, setLoading] = useState(true)
useEffect(() => {
const unsubscribe = firebase.db.collection('occasions')
.onSnapshot(snapshot => {
if (snapshot.size) {
let occasionList = []
snapshot.forEach(doc =>
occasionList.push({ ...doc.data(), uid: doc.id }),
)
setOccasions(occasionList)
setLoading(false)
} else {
setOccasions([])
setLoading(false)
}
})
return () => {
unsubscribe()
}
}, [])
return { occasions, loading }
}
Finally, in your component where you require the data you can access this hook:
const { occasions, loading } = useOccasion()

Resources