How to access the state using Redux RTK in react native app - react-native

I'm using Redux RTK query in my react native project and I can't figure out how to access the state without firing the query each time.
I created an api with my customBaseQuery
const emptySplitApi = createApi({
reducerPath: 'api',
baseQuery: customBaseQuery(),
endpoints: () => ({})
})
I then injected my endpoints.
export const userApi = emptySplitApi.injectEndpoints({
endpoints: (builder) => ({
getUser: builder.query<User, GetUserInput>({
query: (options) => ({
query: getUserQuery,
options: options
})
})
}),
overrideExisting: true
})
And configured the store:
const persistConfig = {
key: 'root',
storage: AsyncStorage
}
const reducers = combineReducers({
[emptySplitApi.reducerPath]: emptySplitApi.reducer
})
const persistedReducer = persistReducer(persistConfig, reducers)
export const store = configureStore({
reducer: persistedReducer,
middleware: (getDefaultMiddleware) => getDefaultMiddleware({ serializableCheck: false }).concat(emptySplitApi.middleware)
})
When I open the app I call the getUser API using the redux-generated hook. This works and I get the expected response.
const { data } = useGetUserQuery(input)
Now, I need to access the user data in another screen of my app without calling the API again (redux should have cached/persisted the data) but I can't manage to access the state.
I tried the following:
const { getUser } = useAppSelector((state) => state.api.queries)
but getUser is always undefined. However if I print the whole state I can see the data.
How can I access the state from anywhere in the app without calling the API again?

Just use your useGetUserQuery(yourOptions) everywhere. It will fire only one request to the server, no matter how many times you use it, and the data will be available everywhere.

Related

Why isn't react native application re-rendering when redux State is changed using useSelector?

I'm trying to build a react native application using expo, firebase, and redux toolkit. With redux toolkit, I have created a slice that has an asyncThunk to get the data, such as a username, from firestore and I have an extra reducer that sets that data to the store.
In my screen file I'm using useSelector() to get the store value, and when I log the value I can see the store is changing but the screen isn't re-rendering. Also, I have a authentication slice in my store as well that works correctly but I don't know if that would mess anything up for this.
Store code:
export const store = configureStore({
reducer: {
firestore: firestoreSlice,
userAuth: authSlice,
},
})
Slice Code is below:
const initialState = {
isLoaded: false,
username: null
}
export const getUsername = createAsyncThunk('firestore/getUsername', async (userId) => {
const firestore = getFirestore();
const docRef = doc(firestore, "users", userId);
const docSnap = await getDoc(docRef);
const data = {
username: docSnap.data().username,
isLoaded: true
}
return data;
})
const firestoreSlice = createSlice({
name: 'firestore',
initialState,
reducers: {},
extraReducers: (builder) => {
builder.addCase(getUsername.fulfilled, (state,action) => {
//state.isLoaded = action.payload.isLoaded;
//state.username = action.payload.username;
return Object.assign({}, state, {username: action.payload.username, isLoaded: action.payload.isLoaded})
})
}
});
export const selectUsername = (state) => state.firestore.username;
export const selectIsLoaded = (state) => state.firestore.isLoaded;
export default firestoreSlice.reducer;
Code inside Home Screen component below:
const username = useSelector(selectUsername);
const isLoaded = useSelector(selectIsLoaded);
useEffect(() => {
dispatch(getUsername(auth.currentUser.uid));
console.log(username + "..." + isLoaded);
}, [username]);
return(
<View style={styles.container}>
<StatusBar></StatusBar>
<Text>Home</Text>
{isLoaded == false ? (
<Text>Welcome, set your username in profile page</Text>
) : (
<Text>Welcome, {username}</Text>
)}
</View>
);
Console Log:
null...false
testing...true
In the console log, I can see the updated username and isLoaded values since UseEffect runs on changes to the username variable. However, the screen isn't re-rendering and I don't see the username displayed. I know that useSelector() only re-renders on reference changes to state, which is why in the extra reducer I tried mutating the state and creating a new object but neither caused the re-render.
Could someone help? Been stuck on this for like a week now!
Thanks!

make redux toolkit query inside useEffect

I am receiving a cat prop and want to fetch data from api whenever cat is changed using RTK query. But it's getting error the following error:
React Hook "useGetProductsQuery" cannot be called inside a callback".
How to make a RTK query whenever cat is changed?
const Products = ({cat,filter,sort}) => {
const [products, setproducts] = useState([])
const [filters, setFilters] = useState([])
useEffect(()=>{
const {data,isError,isLoading}=useGetProductsQuery()
console.log(data,isError,isLoading)
},[cat])
}
import { createApi, fetchBaseQuery }
from'#reduxjs/toolkit/query/react';
const baseUrl=process.env.REACT_APP_ECOMMERCE_API_URL
const createRequest = (url) => ({ url });
export const ecommApi = createApi({
reducerPath: 'ecommApi',
baseQuery: fetchBaseQuery({baseUrl}),
endpoints: (builder) => ({
getProducts: builder.query({
query: () => createRequest("/api/v1/products")
}),
})
})
export const{useGetProductsQuery}=ecommApi;
Why not just pass cat as an argument to the useQuery hook without the useEffect?
That will make a new request every time cat changes.
const {data,isError,isLoading}=useGetProductsQuery(cat)
As an addition to phry's answer:
If "cat" might be undefined use the skip parameter for conditional fetching:
const {data,isError,isLoading} = useGetProductsQuery(cat, {skip: !cat})
See https://redux-toolkit.js.org/rtk-query/usage/conditional-fetching

How to update API path dynamically in VUEX state

I am trying to dynamically update the API path in my Vuex state. Vuex must have a default path "example.com/api/datasetA.json" set when the page loaded and I want to update the path to "example.com/api/datasetB.json" by the user interaction and fetch the new API data immediately.
The relevant part of my code is as follows (updated code):
VUEX:
export const state = () => ({
apiData: [],
apiId: 'datasetA.json'
});
export const mutations = {
fillApiData: (state, data) => {state.apiData = data},
updateApi: (state, newApiId) => {state.apiId = newApiId;}
};
export const actions = {
async getApiData({commit, state}) {
const response = await this.$axios.$get('https://example/api/'+state.apiId);
commit('fillApiData', response);
then VUE method as follows:
methods: {
updateApi(apiId) {
this.$store.commit('updateApi', apiId)
}
Create a mutation that changes the vuex state. Then run this mutation(commit) in the getApiData function
export const state = () => ({
apiData: [],
apiId: 'datasetA.json'
});
export const mutations = {
updateAPI(state, newApiId ) {
state.apiId = newApiId;
}
};
export const actions = {
async getApiData({commit, state}) {
const response = await this.$axios.$get('https://example/api/'+state.apiId);
commit('updateValue', response);
commit('updateAPI', 'some.new.datasetB.json');
}
}
I can update the state directly by using this.$store.state.apiId = apiId in methods but I know this is bad practice
You are correct. However, if you would like that approach to update the state outside Vuex, you can use mutations to change the Vuex - This is good practice.
Then you can do
this.$store.commit('updateAPI', 'my new value')

Accessing Vuex store in Nuxt project from JS file

In my Nuxt project I have a file named "apiAccess.js" in the root folder. This file simply exports a bunch of functions that make Ajax calls to the server API. This file is imported in any page that needs access to the server API. I need to send a JWT token with each of these api requests, and I have stored that token in the Vuex store.
I need to access the JWT token from the Vuex store within this "apiAccess.js" file. Unfortuntaely, this.$store is not recognized within this file. How do I access the Vuex store from within this file? Or should I have done something differently?
Here's a snippet from the apiAccessjs file where I try to access the store:
import axios from 'axios'
const client = axios.create({
baseURL: 'http://localhost:3000/api',
json: true,
headers: { Authorization: 'Bearer' + this.$store.state.auth.token }
})
After i readed this post i used this generic structure:
// generic actions file
import {
SET_DATA_CONTEXT,
SET_ITEM_CONTEXT
} from '#/types/mutations'
// PAGEACTIONS
export const getDataContext = api => async function ({ commit }) {
const data = await this[api].get()
commit(SET_DATA_CONTEXT, data)
}
export const getItemContext = api => async function ({ commit }, id) {
const data = await this[api].getById(id)
commit(SET_ITEM_CONTEXT, data)
}
export const createItemContext = api => async function ({}, form) {
await this[api].create(form)
}
export const updateItemContext = api => async function ({}, form) {
await this[api].update(form)
}
export const deleteItemContext = api => async function ({}, id) {
await this[api].delete(id)
}
and for any store i used actions from my generic file:
// any store file
import {
getDataContext,
getItemContext,
createItemContext,
updateItemContext,
deleteItemContext,
setDynamicModal
} from '#/use/store.actions'
const API = '$rasterLayerAPI'
export const state = () => ({
dataContext: [],
itemContext: {},
})
export const actions = {
createItemContext: createItemContext(API),
getDataContext: getDataContext(API),
getItemContext: getItemContext(API),
updateItemContext: updateItemContext(API),
deleteItemContext: deleteItemContext(API),
}
because I had many stores with similar features.
and the same for mutations i used generic mutations functions.

how to handle failed silent auth error in auth0

I followed spa react quick start guide and it worked fine for more than a month. Recently i had this error and it is logged on auth0 as 'failed silent error' with no further information. I have been told that it is because of the browsers cookie updates and recommended to use new beta release of auth0-spa-js and change cache location to local storage. And it didn't work either.
The code is as follows:
auth_config.json:
{
"domain": "dev.........eu.auth0.com",
"clientId": "....eEKkQ.............",
"redirect_uri": "https://localhost:8080",
"audience": "https://.......herokuapp.com/v1/....",
"cacheLocation": "localstorage"
}
and
react-auth0-wrapper.js:
import React, { useState, useEffect, useContext } from "react";
import createAuth0Client from "#auth0/auth0-spa-js";
const DEFAULT_REDIRECT_CALLBACK = () =>
window.history.replaceState({}, document.title, window.location.pathname);
export const Auth0Context = React.createContext();
export const useAuth0 = () => useContext(Auth0Context);
export const Auth0Provider = ({
children,
onRedirectCallback = DEFAULT_REDIRECT_CALLBACK,
...initOptions
}) => {
const [isAuthenticated, setIsAuthenticated] = useState();
const [user, setUser] = useState();
const [auth0Client, setAuth0] = useState();
const [loading, setLoading] = useState(true);
const [popupOpen, setPopupOpen] = useState(false);
useEffect(() => {
const initAuth0 = async () => {
const auth0FromHook = await createAuth0Client(initOptions);
setAuth0(auth0FromHook);
if (window.location.search.includes("code=")) {
const { appState } = await auth0FromHook.handleRedirectCallback();
onRedirectCallback(appState);
}
const isAuthenticated = await auth0FromHook.isAuthenticated();
setIsAuthenticated(isAuthenticated);
if (isAuthenticated) {
const user = await auth0FromHook.getUser();
setUser(user);
}
setLoading(false);
};
initAuth0();
// eslint-disable-next-line
}, []);
const loginWithPopup = async (params = {}) => {
setPopupOpen(true);
try {
await auth0Client.loginWithPopup(params);
} catch (error) {
console.error(error);
} finally {
setPopupOpen(false);
}
const user = await auth0Client.getUser();
setUser(user);
setIsAuthenticated(true);
};
const handleRedirectCallback = async () => {
setLoading(true);
await auth0Client.handleRedirectCallback();
const user = await auth0Client.getUser();
setLoading(false);
setIsAuthenticated(true);
setUser(user);
};
return (
<Auth0Context.Provider
value={{
isAuthenticated,
user,
loading,
popupOpen,
loginWithPopup,
handleRedirectCallback,
getIdTokenClaims: (...p) => auth0Client.getIdTokenClaims(...p),
loginWithRedirect: (...p) => auth0Client.loginWithRedirect(...p),
getTokenSilently: (...p) => auth0Client.getTokenSilently(...p),
getTokenWithPopup: (...p) => auth0Client.getTokenWithPopup(...p),
logout: (...p) => auth0Client.logout(...p)
}}
>
{children}
</Auth0Context.Provider>
);
};
What is wrong with this code, any help appreciated. Or i can use a different method, i just followed the docs, it doesn't matter as long as it authenticates.
Thanks
I know this has been hanging around for a bit, but i was running into a similar issue.
As I understand it the createAuth0Client helper factory runs the getTokenSilently function by default as part of the set up to re-authenticate users every browser refresh. The problem i was having was that the call to getTokenSilently was erroring, meaning that auth0FromHook was never set and the auth0client never set in state. Because auth0client was undefined, it was then impossible to call loginwithredirect, which is the behaviour i wanted to achieve.
Basically i wanted it to auth silently, but if it failed, send to the log in screen, but that's impossible because the auth0client was undefined, resulting in a cannot call loginwithredirect of undefined error. It seems that (sadly) in the current stable version of the #auth0/auth0-spa-js library (1.6.5 at time of writing) there is no way to bypass getTokenSilently when initialising the client. However in the current beta (1.7.0-beta.5) (Here is a list of versions) they have exposed the Auth0Client class itself, so if you want to move to that version the code could be tweaked with something like....
initAuth0().catch( e => {
const newClient = new Auth0Client(initOptions);
setAuth(newClient);
})
and then in any protected components you can check the loading is finished and if isAuthenticated is still falsey, you should be able to redirect to login despite an error occurring during the getSilentToken.
== NON BETA OPTION
The alternative in the current api would be to perhaps set max_age to 0 or 1 in the initOptions, to force a re-login, and maybe setting prompt to "login" on the second attempt to initialize the authClient