Preventing a Redux dispatch if an updateDoc() fails in Firebase? - reactjs

I want to be able to make a dispatch only if a document in Firebase gets updated, I have two categorised error at hand, a Firebase error, and no connection error, in other words, how to know for sure that the updateDoc() has passed so I can make the dispatch to change the current state according to that update. If not, of course show an error on the UI, the issue here is I am passing the dispatch after the await function. If I were using mongo I would handle this in the response.
const handelColor = (e: React.FormEvent<HTMLInputElement>): void => {
const updateData = async () => {
const docRefCol = doc(db, 'collection', currentUser.uid);
throw 'simulated no premission error';
await updateDoc(docRefCol, { customColor: 'red' });
};
throw 'simulated no connection error';
updateData().catch((error) => {
const errorCode = error.code;
alert(errorCode);
});
throw 'simulated faild updated data erorr';
alert('dispatch to local state');
//dispatch(addColor({ select: 'customColor', value: colors[e.currentTarget.value] }));
};
I did come up with the following solution but still I think I still can do better.
const updateData = async () => {
if (!window.navigator.onLine) {
throw { code: 'Please check your connection' };
}
const docRefCol = doc(db, 'collection', currentUser.uid);
await await updateDoc(docRefCol, { customColor: 'red' });
dispatch(addColor({ select: 'customColor', value: colors[e.currentTarget.value] }));
};
updateData().catch((error) => {
const errorCode = error.code;
alert(errorCode);
});

You can try to follow this code (untested) to avoid redundancy in calling your updateData:
const updateData = async () => {
if (!window.navigator.onLine) {
throw { code: 'Please check your connection' };
}
const docRefCol = doc(db, 'collection', currentUser.uid);
await updateDoc(docRefCol, { customColor: 'red' })
.then(() => {
dispatch(addColor({ select: 'customColor', value: colors[e.currentTarget.value] }));
})
.catch((error) => {
alert(error);
});
};
For more reference, you can check this related StackOverflow post that also discusses what you're trying to achieve.

Related

How to assign objects to an array

I am querying data from firebase, I then want to assign the fetch data to be an array
This is my code
let list = [];
dispatch(fetchUsersPending());
db.collection("users").get().then((querySnapshot) => {
querySnapshot.forEach((doc) => {
list.push({...doc.data()});
console.log('All Users: ', list);
dispatch(fetchUsersSuccess(list));
});
}).catch((error) => {
var errorMessage = error.message;
console.log('Error fetching data', errorMessage);
dispatch(fetchUsersFailed({ errorMessage }));
});;
But in my console am getting an error showing Error fetching data Cannot add property 1, object is not extensible in react firebase
I think your current approach also causes to many unnecessary dispatches to the store. With the following solution you only map your array to documents once and then dispatch them all at once.
With async/await
const fetchData = async () => {
try {
dispatch(fetchUsersPending());
const users = (await db.collection('users').get()).docs.map((doc) => ({ ...doc.data()}));
dispatch(fetchUsersSuccess(users));
} catch (errorMessage) {
dispatch(fetchUsersFailed({ errorMessage }));
}
}
With .then()
const fetchData = () => {
dispatch(fetchUsersPending());
const users = db.collection('users').get().then((snapshot) => {
const users = snapshot.docs.map((doc) => ({ ...doc.data() }));
dispatch(fetchUsersSuccess(users));
}).catch((errorMessage) {
dispatch(fetchUsersFailed({ errorMessage }));
});
}

Spread types may only be created types error in ReactJS

I need your help. I follow this code sandbox Codesanbox selectallcheckbox to implement select all functionality but encountered error Spread types may only be created from object types. Already followed to code but I don't know where I went wrong. Here is my current code:
Retrieve data from api:
const [dataList, setDataList] = useState([]);
const fetchData = async () => {
return await api
.get("/admin-reports/users")
.then((res) => {
setDataList(res.data);
})
.catch((err) => console.log(err));
};
useEffect(() => {
fetchData();
}, []);
Select All handler:
const selectAll = (e: any) => {
const { name, checked } = e.target;
if (name == "selectAll") {
let checkedData = dataList.map((d) => {
return { ...d, isChecked: checked };
});
setDataList(checkedData);
}
};
Include sample data, console logging inside map:

What's causing this async function to work only after a page refresh?

I've been attempting to find discussions about this for over a week now, but most issues seem related to trouble persisting through a refresh, while I'm having state troubles without refreshing, so I'm not getting much of anywhere with it.
I'm attempting to load a gallery of images after a user logs in. The login is functioning properly--updates the state with a reducer and pushes from /login to /gallery and I can see in the inspector that the user ID updates from null to a value.
At /gallery I attempt to retrieve some data through axios asynchronously. It's a POST request so that I can send the user's ID in the body rather than the url/using params.
On initial login, state.images doesn't update and throws this error:
"data: "Cast to string failed for value "{ user: '60fc726d827a4e3daff47619' }" (type Object) at path "user" for model "Upload"" " Relative to my database/models: I've tried adjusting type on the model, both the $type approach and adjusting for a String that is an array, the former caused errors large enough for the page not to load, the latter affected no discernible changes.
If I reload the page, everything works and the images load. If I click from gallery to home and then back to gallery again, nothing changes in the state. I have no idea if this is an issue with my amateur async function structure, my mongodb setup, my reducer, the axios post itself, or something else entirely.
I've read that pretty much everything needs to be lined up inside of useEffect() but I've had absolutely no luck getting that to function either.
The whole of the code is at https://github.com/polysnacktyl/react-foraging, but here's the (seemingly) most relevant:
Login
function Login() {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const { getLoggedIn } = useContext(AuthContext);
const { dispatch } = useContext(Context);
const history = useHistory();
async function login(e) {
e.preventDefault();
try {
const { data } = await axios.post('http://localhost:3000/auth/login', {
email,
password
});
await getLoggedIn();
dispatch({
type: 'login',
payload: { user: data._id }
})
window.localStorage.setItem('user', JSON.stringify(data._id))
history.push('/gallery');
} catch (err) {
console.error(err);
}
}
return (... and so forth
Image Gallery
const { state, dispatch } = useContext(Context);
const [isLoading, setLoading] = useState(true);
const [images, setImages] = useState({ images: [] });
const user = state.user;
const success = async () => {
try {
const res = await axios.post('http://localhost:3000/auth/mine', { user });
dispatch({
type: 'fetchSuccess',
payload: { images: res.data }
})
setImages(res.data);
} catch (err) { console.log(err.response) }
}
const fail = (error) =>
dispatch({
type: 'fetchFail',
payload: { error: error.message }
});
function loadImages() {
dispatch({ type: 'fetchImages' });
setTimeout(async () => {
try {
await success();
setLoading(false)
} catch (error) {
await fail(error);
}
}, 1000);
}
useEffect(() => {
loadImages()
//eslint-disable-next-line
}, [])
if (isLoading) {
return (<div className='loading'>...loading</div>)
} else {
return (...and so on
Let's have a look at your loadImages. This function is called during componentDidMount. This seems OK on first glance but it is a bug actually.
loadImages internally calls success that actually depends on user variable.
Having this in mind what we can do is following:
useEffect(() => {
if(!user) { // if no user present somehow, let's return
return;
}
loadImages() // load the images
//eslint-disable-next-line
}, [user]) // add a dependency to the user, since we load user images actually
I think with this approach the issue will be fixed and also you can remove the setTimeout in loadImages in my opinion.
As it turns out, my issue was in my axios-post-with-req-body strategy. I switched it to a GET request and sent the user ID in the params and now everything is loading on initial login and persisting through page relaods. I'm still not sure why it was causing the behavior it was, but at least I know how to avoid it.
the setup that ultimately worked out for me:
function Gallery() {
const { state, dispatch } = useContext(Context);
const [isLoading, setLoading] = useState(true);
const [images, setImages] = useState([]);
const user = state.user;
const success = async () => {
const res = await axios.get('http://localhost:3000/auth/mine', {
params: { user }
})
dispatch({
type: 'fetchSuccess',
payload: res.data
})
setImages(res.data);
}
const fail = (error) =>
dispatch({
type: 'fetchFail',
payload: { error: error.message }
});
function loadImages() {
dispatch({ type: 'fetchImages' });
setTimeout(async () => {
try {
await success();
setLoading(false)
} catch (error) {
await fail(error);
}
}, 0);
}
useEffect(() => {
loadImages()
//eslint-disable-next-line
}, [])
if (isLoading) {
return (<div className='loading'>...loading</div>)
} else {
return (...images that actually load. wonderful.)
Additionally, I had to adjust the router to accept req.params.user instead of req.body.user.

How to fire an event React Testing Library

I have some code, in a hook, to detect whether the browser is online / offline:
export function useConnectivity() {
const [isOnline, setNetwork] = useState(window.navigator.onLine);
const updateNetwork = () => {
setNetwork(window.navigator.onLine);
};
useEffect(() => {
window.addEventListener('offline', updateNetwork);
window.addEventListener('online', updateNetwork);
return () => {
window.removeEventListener('offline', updateNetwork);
window.removeEventListener('online', updateNetwork);
};
});
return isOnline;
}
I have this basic test:
test('hook should detect offline state', () => {
let internetState = jest.spyOn(window.navigator, 'onLine', 'get');
internetState.mockReturnValue(false);
const { result } = renderHook(() => useConnectivity());
expect(result.current.valueOf()).toBe(false);
});
However, I want to run a test to see whether it returns the correct value when an offline event is triggered, not just after the mocking of the returned value on render. What is the best way to approach this? Where I have got so far is this:
test('hook should detect offline state then online state', async () => {
const { result, waitForNextUpdate } = renderHook(() => useConnectivity());
act(() => {
const goOffline = new window.Event('offline');
window.dispatchEvent(goOffline);
});
await waitForNextUpdate();
expect(result.current).toBe(false);
});
I'm not sure about 'best', but this is one way: change the mock response halfway through the test, and tweak some of the async code:
test('hook should detect online state then offline state', async () => {
const onLineSpy = jest.spyOn(window.navigator, 'onLine', 'get');
// Pretend we're initially online:
onLineSpy.mockReturnValue(true);
const { result, waitForNextUpdate } = renderHook(() => useConnectivity());
await act(async () => {
const goOffline = new window.Event('offline');
// Pretend we're offline:
onLineSpy.mockReturnValue(false);
window.dispatchEvent(goOffline);
await waitForNextUpdate();
});
expect(result.current).toBe(false);
});

React Hooks - How to test changes on global providers

I'm trying to test the following scenario:
A user with an expired token tries to access a resource he is not authorized
The resources returns a 401 error
The application updates a global state "isExpiredSession" to true
For this, I have 2 providers:
The authentication provider, with the global authentication state
The one responsible to fetch the resource
There are custom hooks for both, exposing shared logic of these components, i.e: fetchResource/expireSesssion
When the resource fetched returns a 401 status, it sets the isExpiredSession value in the authentication provider, through the sharing of a setState method.
AuthenticationContext.js
import React, { createContext, useState } from 'react';
const AuthenticationContext = createContext([{}, () => {}]);
const initialState = {
userInfo: null,
errorMessage: null,
isExpiredSession: false,
};
const AuthenticationProvider = ({ authStateTest, children }) => {
const [authState, setAuthState] = useState(initialState);
return (
<AuthenticationContext.Provider value={[authStateTest || authState, setAuthState]}>
{ children }
</AuthenticationContext.Provider>);
};
export { AuthenticationContext, AuthenticationProvider, initialState };
useAuthentication.js
import { AuthenticationContext, initialState } from './AuthenticationContext';
const useAuthentication = () => {
const [authState, setAuthState] = useContext(AuthenticationContext);
...
const expireSession = () => {
setAuthState({
...authState,
isExpiredSession: true,
});
};
...
return { expireSession };
}
ResourceContext.js is similar to the authentication, exposing a Provider
And the useResource.js has something like this:
const useResource = () => {
const [resourceState, setResourceState] = useContext(ResourceContext);
const [authState, setAuthState] = useContext(AuthenticationContext);
const { expireSession } = useAuthentication();
const getResource = () => {
const { values } = resourceState;
const { userInfo } = authState;
return MyService.fetchResource(userInfo.token)
.then((result) => {
if (result.ok) {
result.json()
.then((json) => {
setResourceState({
...resourceState,
values: json,
});
})
.catch((error) => {
setErrorMessage(`Error decoding response: ${error.message}`);
});
} else {
const errorMessage = result.status === 401 ?
'Your session is expired, please login again' :
'Error retrieving earnings';
setErrorMessage(errorMessage);
expireSession();
}
})
.catch((error) => {
setErrorMessage(error.message);
});
};
...
Then, on my tests, using react-hooks-testing-library I do the following:
it.only('Should fail to get resource with invalid session', async () => {
const wrapper = ({ children }) => (
<AuthenticationProvider authStateTest={{ userInfo: { token: 'FOOBAR' }, isExpiredSession: false }}>
<ResourceProvider>{children}</ResourceProvider>
</AuthenticationProvider>
);
const { result, waitForNextUpdate } = renderHook(() => useResource(), { wrapper });
fetch.mockResponse(JSON.stringify({}), { status: 401 });
act(() => result.current.getResource());
await waitForNextUpdate();
expect(result.current.errorMessage).toEqual('Your session is expired, please login again');
// Here is the issue, how to test the global value of the Authentication context? the line below, of course, doesn't work
expect(result.current.isExpiredSession).toBeTruthy();
});
I have tried a few solutions:
Rendering the useAuthentication on the tests as well, however, the changes made by the Resource doesn't seem to reflect on it.
Exposing the isExpiredSession variable through the Resource hook, i.e:
return {
...
isExpiredSession: authState.isExpiredSession,
...
};
I was expecting that by then this line would work:
expect(result.current.isExpiredSession).toBeTruthy();
But still not working and the value is still false
Any idea how can I implement a solution for this problem?
Author of react-hooks-testing-library here.
It's a bit hard without being able to run the code, but I think your issue might be the multiple state updates not batching correctly as they are not wrapped in an act call. The ability to act on async calls is in an alpha release of react (v16.9.0-alpha.0) and we have an issue tracking it as well.
So there may be 2 ways to solve it:
Update to the alpha version and a move the waitForNextUpdate into the act callback
npm install react#16.9.0-alpha.0
it.only('Should fail to get resource with invalid session', async () => {
const wrapper = ({ children }) => (
<AuthenticationProvider authStateTest={{ userInfo: { token: 'FOOBAR' }, isExpiredSession: false }}>
<ResourceProvider>{children}</ResourceProvider>
</AuthenticationProvider>
);
const { result, waitForNextUpdate } = renderHook(() => useResource(), { wrapper });
fetch.mockResponse(JSON.stringify({}), { status: 401 });
await act(async () => {
result.current.getResource();
await waitForNextUpdate();
});
expect(result.current.errorMessage).toEqual('Your session is expired, please login again');
expect(result.current.isExpiredSession).toBeTruthy();
});
Add in a second waitForNextUpdate call
it.only('Should fail to get resource with invalid session', async () => {
const wrapper = ({ children }) => (
<AuthenticationProvider authStateTest={{ userInfo: { token: 'FOOBAR' }, isExpiredSession: false }}>
<ResourceProvider>{children}</ResourceProvider>
</AuthenticationProvider>
);
const { result, waitForNextUpdate } = renderHook(() => useResource(), { wrapper });
fetch.mockResponse(JSON.stringify({}), { status: 401 });
act(() => result.current.getResource());
// await setErrorMessage to happen
await waitForNextUpdate();
// await setAuthState to happen
await waitForNextUpdate();
expect(result.current.errorMessage).toEqual('Your session is expired, please login again');
expect(result.current.isExpiredSession).toBeTruthy();
});
Your appetite for using alpha versions will likely dictate which option you go for, but, option 1 is the more "future proof". Option 2 may stop working one day once the alpha version hits a stable release.

Resources