Trouble dispatching async action from within actions creator - react-native

I'm trying to start a loading spinner when an image uploads as part of a form, and stop it when a reference to the image is saved in Firebase.
This function in my Actions.js file returns the input from a given form field:
export const formUpdate = ({ prop, value }) => {
alert('update')
return {
type: FORM_UPDATE,
payload: { prop, value }
};
};
With Connect, I use formUpdate to store values for different form fields in my Form.js component - this works fine.
I use a separate function in Actions.js to handle the image upload, and once uploaded, I call this function to save a reference in Firebase:
export const saveImageReference = (downloadUrl, sessionId) => {
const { currentUser } = firebase.auth();
firebase
.database()
.ref(`users/${currentUser.uid}/images`)
.push({
imageId: sessionId,
imageUrl: downloadUrl
})
.then(formUpdate({ prop: 'loading', value: false }));
};
I'm trying to get my form to show a loading spinner during upload. To do this, I'm using formUpdate at the end of saveImageReference to dispatch a loading prop. However, this isn't working.
formUpdate executes as part of the .then() block - I see the alert to confirm this - but no data makes it to the Form component.
I've also tried using a different prop (for example 'name') to see if it updates the form field, but nothing happens.
I have redux-thunk working properly - I use a similar approach to show a spinner in my login form - but this action doesn't seem to want to play ball.
If it helps, here's mapStateToProps from my Form component:
const { name, location, loading } = state.testForm;
return {
loading,
name,
activity
};
};
export default connect(
mapStateToProps,
{ formUpdate, uploadImage }
)(Form);
Update
Here's the uploadImage code based on azundo's answer. This doesn't execute:
export const uploadImage = (
uri,
mime = 'application/octet-stream'
) => dispatch => {
const { Blob } = RNFetchBlob.polyfill;
window.XMLHttpRequest = RNFetchBlob.polyfill.XMLHttpRequest;
window.Blob = Blob;
const { currentUser } = firebase.auth();
console.log('Starting upload action...');
return new Promise((resolve, reject) => {
console.log('in promise 1');
const uploadUri = Platform.OS === 'ios' ? uri.replace('file://', '') : uri;
const sessionId = new Date().getTime();
// create a reference in firebase storage for the file
let uploadBlob = null;
const imageRef = firebase
.storage()
.ref(`user/${currentUser.uid}/images`)
.child(`image_${sessionId}`);
// encode data with base64 before upload
RNFetchBlob.fs
.readFile(uploadUri, 'base64')
.then(data => {
console.log('Encoding image...');
return RNFetchBlob.polyfill.Blob.build(data, {
type: `${mime};BASE64`
});
})
// put blob into storage reference
.then(blob => {
uploadBlob = blob;
console.log('uploading...');
return imageRef.put(blob, { contentType: mime });
})
.then(() => {
console.log('Getting download URL...');
uploadBlob.close();
return imageRef.getDownloadURL();
})
.then(url => {
console.log('Saving reference...');
// setLoading();
resolve(url);
saveImageReference(url, sessionId);
})
.then(() => {
dispatch(formUpdate({ prop: 'loading', value: false }));
})
.catch(error => {
reject(error);
});
});
};

Based on what you've described, the formUpdate call in saveImageReference is not actually dispatching your action, it's just calling the bare formUpdate function which simply returns the plain action object. You'll need to find some way to actually get that action dispatched.
Assuming uploadImage is a redux-thunk action I would recommend keeping the knowledge of the action dispatch out of the saveImageReference function and instead dispatch from uploadImage:
export const saveImageReference = (downloadUrl, sessionId) => {
const { currentUser } = firebase.auth();
// note that we are now returning the firebase promise here
return firebase
.database()
.ref(`users/${currentUser.uid}/images`)
.push({
imageId: sessionId,
imageUrl: downloadUrl
});
};
const uploadImage = (arg1, arg2) => dispatch => {
// other upload code here prior to calling the firebase function...
saveImageReference(downloadUrl, sessionId).then(() => {
dispatch(formUpdate({prop: 'loading', value: false}));
});
})

If you trying to render loader, when you await for an async operation. You could use suspense.
This would be a better option.
const OtherComponent = React.lazy(() => import('./OtherComponent'));
function MyComponent() {
return (
<React.Suspense fallback={<Spinner />}>
<div>
<OtherComponent />
</div>
</React.Suspense>
);
}

After much investigation, I solved the issue. I moved saveImageReference() from Actions.js to my component:
addImage = () => {
ImagePicker.showImagePicker(response => {
if (!response.didCancel) {
// shows modal with a form for user to select an image and add metadata
this.setState({ showModal: true });
// set the loading spinner in the form to show image is uploading
this.props.formUpdate({ prop: 'loading', value: true });
// takes the selected image and invokes uploadImage
uploadImage(response.uri).then(url => {
// once image is uploaded, generate sessionId in the component, and invoke saveImageReference
const sessionId = new Date().getTime();
this.props.saveImageReference(url, sessionId);
});
}
});
};
The uploadImage() action creator resolves with the URL of a successfully uploaded image, which saveImageReference() uses to create the reference.
Once that reference is saved, saveImageReference() dispatches a dedicated action to set loading to false. Here's the contents of Actions.js:
export const uploadImage = (uri, mime = 'application/octet-stream') => {
const { Blob } = RNFetchBlob.polyfill;
window.XMLHttpRequest = RNFetchBlob.polyfill.XMLHttpRequest;
window.Blob = Blob;
const { currentUser } = firebase.auth();
console.log('Starting upload action...');
return new Promise((resolve, reject) => {
console.log('in promise');
const uploadUri = Platform.OS === 'ios' ? uri.replace('file://', '') : uri;
const sessionId = new Date().getTime();
// create a reference in firebase storage for the file
let uploadBlob = null;
const imageRef = firebase
.storage()
.ref(`user/${currentUser.uid}/images`)
.child(`image_${sessionId}`);
// encode data with base64 before upload
RNFetchBlob.fs
.readFile(uploadUri, 'base64')
.then(data => {
console.log('Encoding image...');
return RNFetchBlob.polyfill.Blob.build(data, {
type: `${mime};BASE64`
});
})
// put blob into storage reference
.then(blob => {
uploadBlob = blob;
console.log('uploading...');
return imageRef.put(blob, { contentType: mime });
})
.then(() => {
console.log('Getting download URL...');
uploadBlob.close();
return imageRef.getDownloadURL();
})
.then(url => {
resolve(url, sessionId);
})
.catch(error => {
reject(error);
});
});
};
export const saveImageReference = (downloadUrl, sessionId) => {
const { currentUser } = firebase.auth();
console.log('Saving reference!');
return dispatch => {
firebase
.database()
.ref(`users/${currentUser.uid}/images`)
.push({
imageId: sessionId,
imageUrl: downloadUrl
})
.then(ref => {
console.log(ref.key);
dispatch(imageUploadComplete());
});
};
};
const imageUploadComplete = () => {
return dispatch => {
return dispatch({
type: IMAGE_CREATE,
payload: false
});
};
};
No matter what I tried, I couldn't dispatch another action from within saveImageReference() - introducing return dispatch would freeze the flow, and without it I'd get dispatch is not defined.
Invoking this at a component level, using this.props.saveImageReference() solved the issue.

Related

Firestore keep loading old changes

I'm trying to create a firestore listener, which handles changes on my collection. After some research, I implement the feature as below.
useEffect(() => {
const firebaseApp = getFirebaseApp();
const db = firestore(firebaseApp);
const handleSnapshotChanges = ( snapshot: FirebaseFirestoreTypes.QuerySnapshot<FirebaseFirestoreTypes.DocumentData> ) => {
const changes = snapshot.docChanges();
changes.forEach((change) => {
if (change.type === "added") {
console.log(change.doc);
console.log(change.type);
}
if (change.type === "modified") {
console.log("Doc modified");
}
if (change.type === "removed") {
console.log("Remove doc");
}
});
};
const query = db.collection("history");
const unsubscribe = query.onSnapshot(handleSnapshotChanges, (err) =>
console.log(err)
);
return () => {
unsubscribe();
};
}, []);
If I doing so, every time I enter the screen where I put the above useEffect, firestore keeps loading all documents in the collection and marks them as added. How can I implement this function properly.

watchEffect does not run when inserting new data into a list

I am making an application which the user can create projects and assign to a user, for that I am consuming an api made in go, the form sends the data to an api to persist the information and then the api returns the input data to me record the data in the array.
My form in Vue 3:
export default {
setup() {
const name = ref('');
const description = ref('');
const endProject = ref('');
const user = ref(0);
const users = ref();
const listProjects = inject('listProjects');
const router = useRouter();
fetch(`http://localhost:8000/api/v1/users/`)
.then((data) => data.json())
.catch((error) => console.error(error))
.then((response) => {
users.value = response;
});
const saveProject = async () => {
if (
name.value === '' ||
description.value === '' ||
endProject.value === '' ||
user.value === ''
) {
console.log('error');
return;
}
await fetch(`http://localhost:8000/api/v1/projects`, {
method: 'POST',
body: JSON.stringify({
name: name.value,
description: description.value,
end_date: new Date(endProject.value.replace('-', '/')),
user: {
id_user: user.value,
},
}),
headers: {
'Content-Type': 'application/json',
},
})
.then((res) => res.json())
.catch((error) => console.error('error:', error))
.then((data) => {
listProjects.value.push(data);
});
router.push({
name: 'Project',
});
};
return {
name,
description,
endProject,
user,
users,
saveProject,
};
},
};
</script>
The problem happens is when I want to update the api which brings all the projects, I consider that the error occurs because the api where it registers and brings all the information are in different views, one in a form and the other in the view principal.
This is my App.vue
<script>
import { ref } from '#vue/reactivity';
import Navbar from './components/layout/Navbar.vue';
import Sidebar from './components/layout/Sidebar.vue';
import { provide, watchEffect } from '#vue/runtime-core';
export default {
components: { Navbar, Sidebar },
setup() {
const listProjects = ref();
watchEffect(() => {
console.log('bdfbdfb');
fetch(`http://localhost:8000/api/v1/projects`)
.then((res) => res.json())
.catch((err) => console.log(err))
.then((response) => {
listProjects.value = response;
});
});
provide('listProjects', listProjects);
return {
listProjects,
};
},
};
</script>
From comments:
I am expecting that every time the view where the array is pushed is done, App.vue executes the watchEffect
Well it is not how watchEffect works. watchEffect is designed to execute some "side effects" (like fetch call) that depend on some reactive data (for example URL for fetch is created using some ID ref). Yours watchEffect does not depend on any reactive data. It only assign new value to a listProjects ref. (in other words, result of the function you pass to watchEffect does not depend on listProjects ref value at all)
You can debug watchEffect (and watch) using onTrack option. See example bellow. 1st watchEffect does not trigger onTrack because it does not read any reactive data. 2nd does.
const app = Vue.createApp({
setup() {
const projects = Vue.ref([])
Vue.watchEffect(() => {
projects.value = []
}, {
onTrack(e) {
console.log('watchEffect 1 is tracking projects')
}
})
Vue.watchEffect(() => {
console.log(projects.value)
}, {
onTrack(e) {
console.log('watchEffect 2 is tracking projects')
}
})
}
}).mount('#app')
<script src="https://unpkg.com/vue#3.1.5/dist/vue.global.js"></script>
<div id='app'>
</div>

How can I test my custom react hook that uses fetch?

I have created a custom react hook which uses fetch from whatwg-fetch. I have tests for the components that make use of the hook and can mock the whole hook, but now am trying to write tests for the hook itself and my goal is to mock the fetch response. This is my hook.
import { useState, useEffect } from "react";
import "whatwg-fetch";
export default function useFetch(url) {
const [data, setData] = useState(undefined);
const [response, setResponse] = useState(undefined)
const [isLoading, setLoading] = useState(true);
const [error, setError] = useState(undefined);
useEffect(() => {
try {
const fetchData = async () => {
const result = await fetch(url);
setResponse(result);
const responseText = await result.text();
setData(responseText);
setLoading(false);
};
fetchData();
} catch (error) {
setError(error);
}
}, [url]);
return { data, response, isLoading, error };
}
export { useFetch }
Currently, this is how my test looks like. Feels like I cannot mock the fetch to return the desired value.
I have tried writing tests by looking at several tutorials with no luck. I have tried the following tutorials:
Test Custom Hooks Using React Hooks Testing Library
Testing custom react hooks with jest
A Complete Guide to Testing React Hooks
UPDATE: Changed tests, my first test passes (resolve) my second one does not. Based on the third tutorial.
NEW TESTS
import React from "react";
import { render, unmountComponentAtNode } from "react-dom";
import { act } from "react-dom/test-utils";
import useFetch from "./useFetch";
function sleep(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
let container = null;
describe("useFetch tests", () => {
beforeEach(() => {
// setup a DOM element as a render target
container = document.createElement("div");
document.body.appendChild(container);
});
afterEach(() => {
// cleanup on exiting
unmountComponentAtNode(container);
container.remove();
container = null;
});
it("useFetch returns data on success", async () => {
function fetchMock(url) {
return new Promise((resolve) => setTimeout(() => {
resolve({
status: 200,
ok: true,
text: () => Promise.resolve({
data: "data from api"
})
});
}, 300));
}
jest.spyOn(global, "fetch").mockImplementation(fetchMock);
act(() => {
render(<TestComponent url="url1" />, container);
});
expect(container.textContent).toBe("loading");
await sleep(500);
expect(container.textContent).toBe("data from api");
});
it("useFetch return error on fail", async () => {
// const a = 200 + Math.random() * 300;
// console.log(a)
// let promise = new Promise(function (resolve, reject) {
// // after 1 second signal that the job is finished with an error
// setTimeout(() => reject("error"), a);
// });
// function fetchMock(url) {
// return promise;
// }
function fetchMock(url) {
return new Promise((resolve) => setTimeout(() => {
resolve({
status: 404,
ok: true,
text: () => Promise.resolve({
data: "data from api"
})
});
}, 200 + Math.random() * 300));
}
jest.spyOn(global, "fetch").mockImplementation(fetchMock);
act(() => {
render(<TestComponent url="url1" />, container);
});
expect(container.textContent).toBe("loading");
await sleep(500);
expect(container.textContent).toBe("error");
});
});
function TestComponent({ url }) {
const {
data, response, isLoading, error
} = useFetch(url);
if (isLoading) {
return <div>loading</div>;
}
if (data) {
return <div>{data.data}</div>
}
if (error) {
return <div>error</div>
}
return <div></div>;
}
OLD TESTS
import { useFetch } from "../../../src/utils/custom-hooks/useFetch";
describe("useFetch tests", () => {
beforeEach(() => {
jest.spyOn(window, "fetch");
});
it("200", () => {
window.fetch.mockResolvedValueOnce({
ok: true,
status: 200,
})
const { result, rerender } = renderHook(
(url) => useFetch(url),
{
url: "url1"
}
);
expect(result.current.response).toBe(undefined);
rerender("url2");
expect(result.current.status).toBe(200);
});
});

Can't get props to work by passing data through vue-router

I found numorous of similar questions, tried almost all of them including defining params value as a string, and so on but still can't make it work.
I have defined the route to accept props as written on the docs:
const routes = [
...
...
{
// this doesn't work, but passed on the url
// path: '/profile/personal/update/:original',
path: '/profile/personal/update',
name: 'profile.personal.update',
component: () => import('#/views/Profile/Personal/PersonalUpdate.vue'),
props: true,
// props: route => ({ default: route.params.original }), // this doesn't work
},
]
const router = createRouter({
history: createWebHistory(process.env.BASE_URL),
routes,
})
export default router
When I try to push the route and pass the data, typescript complains that I can't pass object to the params property, so I tried to cast it as any and string by doing like this, it passed:
// Somewhere inside the usePersonalInfoComposition
const router = useRouter()
const personalInfo = reactive({
data: new PersonalInfo(),
isLoading: true,
})
const editPersonalInfo = () => {
logger.info('Preparing data to be edited and redirect')
router.push({
name: 'profile.personal.update',
params: {
original: JSON.stringify(personalInfo.data)
// original: personalInfo.data as any
}
})
}
However, when I try to grab the value of the prop which the value is given from route params, I got undefined.
export default defineComponent({
props: ['original'],
setup(props) {
const router = useRouter()
const { personalInfo, updatePersonalInfo } = usePersonalInfoComposition()
onBeforeMount(() => console.log(props.original)) // undefined
onMounted(() => console.log(props.original)) // undefined
// I would do this personally but it was kinda problematic
// and error-prone especially when the page is refreshed.
// So I decided to not doing it. Also, the `props` are now useless.
onBeforeMount(() => {
const original = router.currentRoute.value.params['original'] // "{ user: { id: 1, name: 'joe' } }"
let parsed;
// I need to do this since `params` returns either `string | string[]`
if (original instanceof Array) parsed = JSON.parse(original[0])
else parsed = JSON.parse(original)
console.log(parsed) // { user: { id: 1, name: 'joe' } }
})
}
})
Is there any way to pass the original data without only passing id and make the user fetch the whole time they refreshes the page? I see Vuex has some solution for this, but I don't want to rebuild the whole app from scratch only for this kind of problem.
I was wondering about using local storage like sqlite or the browser's built in local storage but I think it will add more unnecessary complexity. How do you think?
After a while, It comes to a solution but with different approach to fix this problem. I'm not a state management expert but it works.
Figuring out the problem
Let's say I have this on the personal info page:
PersonalInfo.vue
export default defineComponent({
name: 'PersonalInfoPage',
setup() {
const {
personalInfo,
fetchPersonalInfo,
editPersonalInfo,
} = usePersonalInfoComposition()
onBeforeMount(() => fetchPersonalInfo())
return {
personalInfo,
}
}
})
And an update page:
PersonalInfoUpdate.vue
export default defineComponent({
name: 'PersonalInfoUpdatePage',
setup() {
const { personalInfo } = usePersonalInfoComposition()
return {
personalInfo,
}
}
})
Both has the same state being used, but from what I see, the composition was creating a new instance every time I call the method so the previously fetched state is not there and I need to re-fetch the same stuff all over again.
Solution
Instead of doing it that way, I lifted the state up above the composition method to keep the fetched state and make a "getters" by using the computed property to be consumed inside my PersonalUpdate.vue
PersonalInfoComposition.ts -- before
export const usePersonalInfoComposition = () => {
const personalInfo = reactive({
data: new PersonalInfo(),
isLoading: true,
})
const editPersonalInfo = () => {
router.push({
name: 'profile.personal.update',
params: {
original: JSON.stringify(personalInfo.data),
}
})
}
const fetchPersonalInfo = () => {
state.isLoading = true
repository
.fetch()
.then(info => state.data = info)
.catch(error => logger.error(error))
.finally(() => state.isLoading = false)
}
return {
personalInfo,
fetchPersonalInfo,
editPersonalInf,
}
}
PersonalInfoComposition.ts -- after
const state = reactive({
data: new PersonalInfo(),
isLoading: true,
})
export const usePersonalInfoComposition = () => {
const personalInfo = computed(() => state.data)
const isLoading = computed(() => state.isLoading)
const editPersonalInfo = () => {
router.push({ name: 'profile.personal.update' })
}
const fetchPersonalInfo = () => {
state.isLoading = true
repository
.fetch()
.then(info => state.data = info)
.catch(error => logger.error(error))
.finally(() => state.isLoading = false)
}
return {
personalInfo,
isLoading,
fetchPersonalInfo,
editPersonalInfo,
}
}
Now it works, and no Vuex or local storage or even route.params needed.
The Downside
Since the state is shared, whenever I made any changes on the state attached to a v-model, the data inside the PersonalInfo.vue are also be changed.
This could be easily fixed by creating a clone of the state object to handle the updates and sync it whenever the new data persisted in the database.
PersonalInfoComposition.ts -- fixing the downside
const state = reactive({
data: new PersonalInfo(),
isLoading: true,
})
const editState = reactive({
data: new PersonalInfo(),
isLoading: true,
})
export const usePersonalInfoComposition = () => {
const personalInfo = computed(() => state.data)
const personalInfoClone = computed(() => editState.data)
const isLoading = computed(() => state.isLoading)
const editIsLoading = computed(() => editState.isLoading)
const editPersonalInfo = () => {
// This will clone the object as well as its reactivity, dont do this
// editState.data = state.data
// Clone the object and remove its reactivity
editState.data = JSON.parse(JSON.stringify(state.data))
router.push({ name: 'profile.personal.update' })
}
const fetchPersonalInfo = () => {
state.isLoading = true
repository
.fetch()
.then(info => state.data = info)
.catch(error => logger.error(error))
.finally(() => state.isLoading = false)
}
const updatePersonalInfo = () => {
editState.isLoading = true
repository
.update(editState.data.id)
.then(info => {
// This could be extracted to a new method, but keep it simple for now
state.data = info
if (state.data === info) router.back()
})
.catch(error => logger.error(error))
.finally(() => editState.isLoading = false)
}
return {
personalInfo,
personalInfoClone,
isLoading,
editIsLoading,
fetchPersonalInfo,
}
}

React Native AsyncStorage.getItem is not working. ({"_40": 0, "_55": null, "_65": 0, "_72": null})

Good day! I have this function of AsyncStorage that gets an item of a token. I used with ApolloClient to process the token but when I test it first, it seems to have an error with what will I get by AsyncStorage function.
export function jwtLogin(data) {
return async dispatch => {
const userData = {
email: data.email,
password: data.password,
};
console.log(userData);
const client = new ApolloClient({
link: new HttpLink({
uri: API_URL,
}),
cache: new InMemoryCache(),
});
client
.mutate({
mutation: loginUser,
variables: {
email: userData.email,
password: userData.password,
},
})
.then(resp => {
console.log(resp.data.tokenCreate);
console.log('Token', resp.data.tokenCreate.token);
if (resp.data.tokenCreate.token !== null) {
saveJWTTokenData(resp.data.tokenCreate.token); //from AsyncStorage save function
async function main() { //function of AsyncStorage
await AsyncStorage.getItem('jwt_token').then(item => {
return item;
});
}
console.log(main()); // returns error
Actions.push('main_loading');
} else {
const errors = resp.data.tokenCreate.errors;
{
errors.map(err => {
Alert.alert('Error.', err.message);
});
}
}
})
.catch(err => {
Alert.alert('Error.', err.message);
});
};
}
For the save storage function:
export const saveJWTTokenData = async jwt_token => AsyncStorage.setItem('jwt_token', jwt_token);
My Error Log Picture
I think your Promise is not handled correctly..
Try to add a catch after your then call like this:
.catch(err => console.log(err))
Or try to use your function like this maybe:
await getData("jwt_token")
.then(data => data)
.then(value => this.setState({ token: value })) // here it is setState but I guess you can also return
.catch(err => console.log("AsyncStorageErr: " + err));