Redux subscribed async State empty - reactjs

I have a Login Handler which takes the response and dispatches data to redux (userdata and json web tokens). I also do have an interval to renew JWTs every X minutes.
However the subscribed state returns undefined and I´m quite unsure why. If I track the redux store there is data written successfully to it. What do I miss here?
const App = () => {
const dispatch = useDispatch()
const logedInUser = useSelector(state => state.logedInUser.logedInUser)
const tokens = useSelector(state => state.token.tokens)
const intervalRef = useRef();
const refreshToken = useCallback(async () => {
console.log(tokens.refreshToken) //prints undefined
console.log(logedInUser.id) //prints undefined
let response = await restAPI("post", hostAPI + "/refreshToken", {token: tokens.refreshToken})
dispatch(setTokens({
accessToken: response.accessToken,
refreshToken: response.refreshToken
}))
}, [])
useEffect(() => {
const interval = setInterval(() => refreshToken(), 30000)
intervalRef.current = interval;
return () => clearInterval(interval)
}, [refreshToken])
const loginHandler = async (data) => {
let response = await restAPI("post", hostAPI + "/login", {email: data.email, password: data.password}) //Simple Fetch with Post method returns response as JSON
if(response.user.id) {
console.log("RESPONSE",response) //Prints correct Response
dispatch(setLogedInUser(response.user)) //Dispatch returned Data to redux
dispatch(setTokens({
accessToken: response.accessToken,
refreshToken: response.refreshToken
}))
}
}
TokenSlice as example (redux toolkit used):
const initialState = {
tokens: []
}
export const tokenSlice = createSlice({
name: "token",
initialState,
reducers: {
setTokens: (state, action) => {
state.tokens = action.payload
console.log("Token:", state.tokens) //Prints out correct Tokens afterDispatch
},
}
})
If I build up a Component with a button in which I Refresh the Token on click everything works as expected. I´m sure that it is just a silly little thing what I´m missing here but since I´m pretty new to Redux I can´t point out what is the issue here is.
Thanks in advance!
Edit:
I´ve noticed something else. If I change some code and Save it the next "Refresh Token Interval" does print out the correct values. Means somehow "tokens" never updates from the Initial empty state - at least in app component. Like mentoined the redux state itself holds the right values.

I think your method of fetching data from API is wrong. I'm seeing that in your code snippets. You are not using any package like Axios or API request mode. Please see any tutorial on how to fetch data from API?
Try to do this work by using :
const Axios = require("axios");
Axios.defaults.withCredentials = true;

Related

How to check if redux actionReducer axios request is successful or not

I'm creating a page login with OTP, so first I sent request for login. If it's successful then my model would open for entering the otp.
But in my case model will open always because im unable to figure out how check if my request is successfult or not
I can manage state but I don't want to manage state for this simple task. I just want that the request in action reducer is successful or not. This will surely solves my problem easily.
const handleSubmit = async (event) => {
event.preventDefault();
const data = new FormData(event.currentTarget);
let email= data.get('email');
let password= data.get('password');
await props.otpRequest(email,password);
handleOTPModelOpen();
};
Action Reducer
export const otpRequest = (email,password) => {
return (dispatch) => {
const url = process.env.REACT_APP_DEV_BACKEND
dispatch(getOTPRequest())
axios
.post(`${url}/api/get_otp`,{
email: email,
password: password
})
.then(response => {
dispatch(getOTPSuccess())
dispatch(showAlert(handleAxiosError(response),"success"));
})
.catch(error => {
dispatch(showAlert(handleAxiosError(error),"error"));
dispatch(getOTPFailure())
throw OTPRequestFailed()
})
}
}
and using mapDispatchToProps
const mapDispatchToProps = dispatch => {
return {
fetchToken: (email,otp) => dispatch(fetchToken(email,otp)),
otpRequest: (email,password) => dispatch(otpRequest(email,password))
}
}
I just want to check if otpRequest is successful or not. Like that we checkedd in axios request in action reducer.
All request are successful no error is coming
One nice way of handling this for most libraries is a try / catch block.
You could do this:
try {
await props.otpRequest(email,password);
handleOTPModelOpen();
} catch (e) {
// handle error here
console.log(e);
handleOTPFailed();
}
EDIT 1
I don't see a selector or use of mapStateToProps callback. You use mapDispatchToProps which is great for the actions, however, in order to access the resulting state, you must also add mapStateToProps. Here's an example from one of my apps:
const mapStateToProps = (state) => {
return {
success: state.data.success;
}
}
Think of state as a whole pie. A Redux selector allows you to take a slice of that pie and return it to the React component props for use, instead of the entire state (response from the API dispatch).

Redux-Toolkit createAsyncThunk request to firestore returns undefined

I am learning react and have started on a personal project using redux, redux-toolkit and firestore to store data. I have a problem with fetching and storing data that I cannot solve. Its entirely possible that I'm using a wrong approach to the problem or I've forced myself to a dead-end from a technical perspective (I am a noob after all), so any suggestions are welcome. On to the matter at hand.
Everything I have made so far works, in the sense that firestore receives the request and stores data as requested, however the code is not technically correct as you will see in the snippets bellow. I've been using createAsyncThunk from redux toolkit to handle requests and it seems to be working, but something seems a bit off in the code. If I use setDoc(), for example, to update a field in firestore, the return value is undefined (i.e. Promise<void>), as per the documentation: https://firebase.google.com/docs/reference/js/firestore_.md#updatedoc and I'm not sure how to handle it. Bellow I have shared the snippets required for making an update on the users document.
Initialize Firebase and define an update function (FIREBASE__CONFIG omitted):
const app = initializeApp(FIREBASE_CONFIG);
const dbFB = getFirestore(app);
export const updateFB = async (url, id, values) => {
const docRef = doc(dbFB, url, id);
const docResponse = await updateDoc(docRef, values);
return docResponse;
};
Define user slice (omitting configureStore here for brevity):
const initialState = {
isLoggedIn: false,
userDetails: null,
};
const userSlice = createSlice({
name: 'user',
initialState,
reducers: {
...
},
extraReducers: (builder) => {
builder.addCase(userUpdate.fulfilled, (state, action) => {
state.userDetails = { ...state.userDetails, ...action.payload };
});
}
});
Define userUpdate action (the requestError dispatch is an update for another slice):
export const userUpdate = createAsyncThunk(
'user/userUpdate',
async ({ userId, values }, { dispatch }) => {
try {
const usersRes = await updateFB('/users', userId, values);
return usersRes;
} catch (err) {
console.log(err);
dispatch(httpActions.requestError({ errorMessage: err.message || 'Something went wrong!'}));
}
}
);
Calling the update request on submit (using FormIK):
<Formik
...
onSubmit={async (values, { setSubmitting }) => {
dispatch(userUpdate({userId: userDetails.userId, values})).then(() => {
setSubmitting(false);
history.push(MY_PROFILE.path);
});
}}
>
{({ isSubmitting }) => (
<Form>
<div className='form-field'>
...
</div>
</Form>
)}
</Formik>
As you can see, I would like to continue execution of some other commands after the request has resolved on form submission. However, the return value of this request is undefined and it feels off to use .then() on it, even though it works.
Perhaps I've gone completely off the beaten path?
Help is appreciated, thank you :)
As you mentioned, the documentation stated that it will return Promise<void>. The reason behind: This is just a simple document write operation sent off to the server. If it did return the document data in the call, not only you'll need to wait for the data to be sent back, and you'll also be billed for a document read and bandwidth that is never needed.
If you need the updated data, you'll need to use getDoc() in a separate call. If you update multiple documents, you may need multiple getDoc() calls too. e.g:
export const updateFB = async (url, id, values) => {
const docRef = doc(dbFB, url, id);
const docResponse = await updateDoc(docRef, values);
// Returns updated document.
const docSnap = await getDoc(docRef);
console.log(docSnap.data());
return docSnap.data();
};

Testing Redux Toolkit Query using jest issue with auth

Im currently trying to write jest test for my RTKQuery, but I get stuck on the authentication level for the test.
Basically the api Im using is designed to have the token on query param instead of having it on the request header: "https://api/v1/something/meta/?token=userToken"
So when I try to test the api call it shows me the request has been rejected. Does anyone know how to write the test with this case?
here is my RTKQuery endpoint:
// index.ts
export const rootApi = createApi({
reducerPath: "root",
baseQuery: fetchBaseQuery({baseUrl: API_ROOT}),
endpoints: () => ({});
})
// dataEndpoint.ts
const token = getToken(); // Gets the user's token from localStorage after user login
export cosnt apiWithData = rootApi.injectEndpoints({
endpoints: (build) => ({
fetchDataMetaList: build.mutation<DataType, any>({
query: ({offset = 0, size = 20, body}) => ({
// token is passed in for query param
url: `${API_URL}?offset=${offset}&size=${size}&token=${token}`,
method: "POST",
body: body || {}
})
})
})
})
below is my test:
// data.test.tsx
const body = { offset: 0, size: 20, body: {} };
const updateTimeout = 10000;
beforeEach((): void => {
fetchMock.resetMocks();
})
const wrapper: React.FC = ({ children }) => {
const storeRef = setupApiStore(rootApi);
return <Provider store={storeRef.store}>{children}</Provider>
}
describe("useFetchDataMetaListMutation", () => {
it("Success", async () => {
fetchMock.mockResponse(JSON.string(response));
cosnt { result, waitForNextupdate } = renderHook(
() => useFetchDataMetaListMutation(),
{ wrapper }
)
const [fetchDataMetaList, initialResponse] = result.current;
expect(initialResponse.data).toBeUndefined();
expect(initialResponse.isLoading).toBe(false);
act(() => {
void fetchDataMetaList(body);
})
const loadingResponse = result.current[1];
expect(loadingResponse.data).toBeUndefined();
expect(loadingResponse.isLoading).toBe(true);
// Up til this point everything is passing fine
await waitForNextUpdate({ timeout: updateTimeout });
const loadedResponse = result.current[1];
// expect loadedResponse.data to be defined, but returned undefined
// console out put for loaded Response status is 'rejected' with 401 access level
// error code
})
})
Doing a top-level const token means that as soon as that file is loaded, it will retrieve that token from the local store and that it will never be able to update that - so if that file is loaded before the user is logged in, it will be empty. That is pretty much also what happens in your test here.
To be honest, this might be the first time ever that I see a token as part of the url (that is a serious security problem as the token would be shared between users on copy-pasting the url, it's visible in the browser history even after logout etc!).
Unfortunately in that case, you cannot use prepareHeaders, but at least you could instead of the const use a function to get the current token - and if you import that from another file, you could also use jest mocking to just switch out that import.

Check setState has run before executing API call in React

I have a form with a text field and a form input which accepts multiple files. OnSubmit the files are sent to Firebase Storage, which sends back a URL for each file. These URLs are then stored in a 'photosURL' array in the form object, which is then posted to MongoDB.
The problem is, every time I post the form object data to Mongo, the photos array is empty, despite the console log showing it to be populated before I call the post-to-Mongo code. This leads me to think the post-to-Mongo code is using the form object value before it has been populated with the photo URLs.
The question is, how do I check that the photo array has been populated before I run the code to push the data to MongoDB? I'm already using a Promise.all to in theory wait for all the files to be sent and the URLs returned, but I can't work out why else the photoURLs array is empty every time data is sent to Mongo.
Here's the code:
const [form, setForm] = useState({
userId: '',
post: '',
createdAt: createdAt,
photoURLs: [],
})
const handleSubmit = (e) => {
e.preventDefault()
newPost ? postData(form) : ...
}
// SEND FILE TO FIREBASE AND GET BACK THE URL
async function handleUpload(file) {
const storageRef = useStorage.ref("PostImages");
const fileRef = storageRef.child(`${nanoid()}`);
return fileRef.put(file).then(() => {
return fileRef.getDownloadURL().then(function (url) {
photoArray.push(url);
setForm(prevState => ({ ...prevState, photos: photoArray }))
});
});
}
// POST FUNCTION
const postData = async (form) => {
setLoading(true)
let thisFileArray = fileInput.current.files;
const uploadTasks = [];
for (let i = 0; i < thisFileArray.length; i++) {
uploadTasks.push(handleUpload(thisFileArray[i]));
}
Promise.all(uploadTasks).then(() => {
axios.post('/api/posts', form)
.then(response => {
...
})
.catch(error => {
...
})
})
}
Can anyone see what's going wrong, please?
EDIT: This is a consolel log of the form object, called before the axios.post code (it's showing the photosURL as populated):
createdAt: 1630072305502
photos:
0: "https://firebasestorage.googleapis.com/..."
1: "https://firebasestorage.googleapis.com/..."
post: "sample text"
userId: "1iNGV..."
I think that you are running into a timing issue.
Don't forget that React state updates are asynchronous, as described here.
I suggest to pass your URLs directly instead of going through your component's state:
async function handleUpload(file) {
const storageRef = useStorage.ref("PostImages");
const fileRef = storageRef.child(`${nanoid()}`);
await fileRef.put(file);
const url = await fileRef.getDownloadURL();
return url; // Send back the download URL
}
const postData = async (form) => {
setLoading(true);
let thisFileArray = fileInput.current.files;
const uploadTasks = [];
for (let i = 0; i < thisFileArray.length; i++) {
uploadTasks.push(handleUpload(thisFileArray[i]));
}
const photos = await Promise.all(uploadTasks); // Get all URLs here
await axios.post('/api/posts', {...form, photos}); // Send URLs to your server
setLoading(false);
}
If I understood correct, You want to upload files first and when you get your urls array populated then only you want to call postData function?
If that's the case, then you can use useEffect to detect the urls change.
useEffect(() => {
// Here you call your postData function.
postData();
}, [form.photoURLs])
What this will do is, Whenever your form.photoURLs gets populated this useEffect will run and will make the request to the server with proper data.

Pulling Redux data with userID param error - React / Node / Express

I am trying to pull a log of user data belonging to userID from my backend in React. The backend API is working correctly with Postman however I can't get this working with the userID. It is console logging the correctly userID in the component however I am yet to get a Thunk Redux store data working with a param passed in.
Is there anywhere obvious I am going wrong from looking at the code below? I have had my Redux working previously with data not using a param so I know it is not an issue with my store / redux index etc.
component
const userDiveLogList = useSelector(state => state.user.userDiveLogList);
const [userDiveLog, setUserDiveLog] = useState({
userDiveLogList: [],
expanded: false
})
const dispatch = useDispatch();
useEffect(() => {
dispatch(requireUserDiveLogData(user.userID));
}, []);
action
// load a users diveLog
export const requireUserDiveLogData = createAsyncThunk('diveLog/requireData',
async (userID, thunkAPI) => {
const response = await userDiveLogList(userID);
return response.data;
},
{
condition: (_, { getState }) => {
const { user } = getState();
if (user.didLoadData) {
return false;
}
}
}
)
reducer
export const userSlice = createSlice({
name: 'user',
initialState: {
userDiveLogList: [],
didLoadData: false,
},
reducers: {
[requireUserDiveLogData.pending.type]: (state) => {
state.didLoadData = true;
},
[requireUserDiveLogData.fulfilled.type]: (state, action) => {
return {
...state,
...action.payload
}
},
service
// returns list of dives per user from db
export const userDiveLogList = (userID) => {
return axios.get(API_URL + `userdiveloglist/${userID}`);
};
Update
Does this have something to do with my action call for when data gets called if it is already called? I have the getState condition const as userDiveLog when it is in the user branch and it is user/userDiveLogList in the branch, it is also named userDiveLogList when it is being sent at the backend. I can see the backend request being made for the correct user number but nothing is returning.

Resources