React Apollo Hooks - Chain mutations - javascript

I have two mutations:
const [createRecord, {data}] = useMutation(createRecordQuery); which returns the ID of the newly created record
const [saveValue, {data: dataSave}] = useMutation(saveValueQuery); which save some values on a record
My saveValue mutation requires a record ID. If I open my form on a new record, I don't have this ID yet, so on submit I need to call createRecord first to retrieve the ID and then saveValue to save values on my record.
This simple approach doesn't work:
const onSave = async values => {
if (!recordId) {
// Call createRecord to retrieve recordId (code is simplified here)
const recordId = await createRecord();
}
// Save my values
return saveValue({variables: {recordId, values});
}
But I don't really know how should I deal with the loading and data of the first mutation and wait for it to run the second mutation.
Thanks!

The mutate function returns a promise that resolves to the mutation response data, so you should simply be able to use to achieve what you want.
From the source code:
If you're interested in performing some action after a mutation has
completed, and you don't need to update the store, use the Promise
returned from client.mutate
I'm not sure why this didn't work in your initial tests, but I tried it locally and it works as expected. You should essentially be able to do what you wrote in your question.

I'm not sure if there is a way to postpone execution(as well as we cannot pause <Mutation>). So how about moving second part into separate useEffect?
const [recordId, setRecordId] = useState(null);
const [values, setValues] = useState({});
const onSave = async _values => {
if (!recordId) {
// Call createRecord to retrieve recordId (code is simplified here)
setRecordId(await createRecord());
}
setValues(_values);
}
useEffect(() => {
saveValue({variables: {recordId, values});
}, [recordId, _values]);
Another workaround is utilizing withApollo HOC:
function YourComponent({ client: { mutate } }) {
onSave = async values => {
let recordId;
if (!recordId) {
recordId = await mutate(createRecordQuery);
}
await mutate(saveValueQuery, values);
// do something to let user know saving is done
};
export withApollo(YourComponent);

Related

I'm fetching a problem when I'm trying to fetch data from an API by using useEffect in react

const [users, setUsers] = useState([]);
const getUsers = async () => {
const URL = "https://jsonplaceholder.typicode.com/users";
const response = await fetch(URL);
const data = await response.json();
console.log(data); // return 10 array .
setUsers(data);
console.log(users); // return empty Array
};
useEffect(() => {
getUsers();
}, []);
when I run this code and reload the browser, the data variable shows 10 arrays (that's what I expected) and the users state doesn't return an empty array (that's the problem). And then I'm changing something on code, and come to the browser, data variable and users state both shows 10 arrays.
That means I'm trying to tell you that, when I'm reloading the browser for the first time state doesn't set from data variable .
See the console result to better understand
Setting a state is an asynchronous process. That's why you won't see the immediate effect after calling the setUsers(). Rather you can add another useEffect in your code and see the changes happening for users
// ... other code
useEffect(() => {
// this will run after the 'users' has been changed
console.log("Users", users)
}, [users]);
// ...
After adding this useEffect you will see that the users will be printed after it is updated.

react useState only stores the last document

I am working on a react Netflix clone web app and I have stored some data on Firestore for making a favorite list. When I fetched data from Firestore and try to store in a state, some error occured.
There were about four documents in firebase but I only got the last one every time I try
I have included key to the map and try to give the state empty string as initial value, I tried spread operator but none of them worked
firestore is working well as i see those documents on console
const [movie, setMovie] = useState([])
useEffect(() => {
async function fetchData() {
const q = query(collection(db, "movie"));
const querySnapshot = await getDocs(q);
querySnapshot.forEach((doc) => {
setMovie([...movie, doc.data().Details])
console.log(doc.data().Details)
console.log(movie)
});
}
fetchData()
Your fetchData function closes over movie, which means it only sees the movie that existed when the function was created (you haven't shown enough to be sure, but I'm guessing you have an empty dependencies array on that useEffect, so that would be an always-empty movie). Doing setMovie([...movie, doc.data().Details]) just spreads that empty movie and then adds the one final document.
Instead, two things:
Whenever updating state based on existing state, it's best to use the callback form of the setter so that you get the up-to-date version of the state you're updating.
Collect the documents, then do just one setter call.
const [movies, setMovies] = useState([])
useEffect(() => {
async function fetchData() {
const q = query(collection(db, "movie"));
const querySnapshot = await getDocs(q);
setMovies(previousMovies => [
...previousMovies,
...querySnapshot.map((doc) => doc.data().Details)
]);
}
fetchData();
}, []); // <== I've assumed this
(Note I've made it a plural, since there's more than one movie.)
(I don't use MongoDB, but I've assumed from the forEach that querySnapshot is an array and so it has map. If not, it's easy enough to create the array, use forEach to push to it, and then do the setMovies call.)
But there's another thing: You should allow for the possibility your component is unmounted before the query completes. If your getDocs has a way to cancel its operation, you'll want to use that. For instance, if it accepted an AbortSignal, it might look like:
const [movies, setMovies] = useState([])
useEffect(() => {
async function fetchData() {
const controller = new AbortController();
const { signal } = controller;
const q = query(collection(db, "movie"));
const querySnapshot = await getDocs(q, signal);
if (!signal.aborted) {
setMovies(previousMovies => [
...previousMovies,
...querySnapshot.map((doc) => doc.data().Details)
]);
}
}
fetchData();
return () => {
controller.abort();
};
}, []);
But if there's some other cancellation mechanism, naturally use that.
If there's no cancellation mechanism, you might use a flag so you don't try to use the result when it won't be used for anything, but that may be overkill. (React has backed off complaining about it when you do a state update after the component is unmounted, as it was usually a benign thing to do.)
Don't know if it will solve the problem, but when you set a state value base on the previous value (here setMovie([...movie, doc.data().Details])) you should use a callback function: https://en.reactjs.org/docs/hooks-reference.html#functional-updates
Like this:
setMovie(oldMovies => ([...oldMovies, doc.data().Details]))
If it still doesn't work try to create a new tab you're filling during the forEach, and at the end, set the state:
const q = query(collection(db, "movie"));
const querySnapshot = await getDocs(q);
const newMovies = []
querySnapshot.forEach((doc) => {
newMovies.push(doc.data().Details)
});
setMovie([...movie, ...newMovies])
for a start, I suggest you to do something like the following:
const [movie, setMovie] = useState([])
useEffect(() => {
async function fetchData() {
const q = query(collection(db, "movie"));
const querySnapshot = await getDocs(q);
setMovie(querySnapshot.flatMap(elt=>elt.data().Details));
}
fetchData()
It's because you update your state on forEach loop, you start from first doc until the last one, in the end you set the last movie, that's why you have everytime the last doc on your state.
First of all don't update state too frequently, it creates app performance problems.
To fix it (based on your example):
let movies=[]
querySnapshot.forEach((doc) => {
movies([...movies, doc.data().Details]);
//or movies.push(doc.data().Details);
});
setMovie(movies);

useState with async function returning Promise {<pending>}

So, I know this question has been asked 100's of times, but none of the solutions seems to work in my instance.
I am using useState hook to update state to a value initialValues that gets data returned from a getInitialValues function
const [initialValues, setInitialValues] = useState(getInitialValues());
The getInitialValues function does a logic check and either returns an object or another function retrieveDetails()
const getInitialValues = () => {
let details;
if(!addressDetails) {
details = retrieveDetails();
} else {
details = {
...,
...,
...
};
}
return details;
}
The function, retrieveDetails is an async function that makes an API call, and I await the response and return the object received from the response.
const retrieveDetails = async () => {
const addr = addressDetails[currentAddress];
const { addressLookup } = addr;
const key = process.env.API_KEY;
const query = `?Key=${key}&Id=${addressLookup}`;
const addrDetails = await new AddrService().getAddressDetails(query);
return addrDetails;
}
However, when I log the state initialValues it returns Promise {<pending>}?
Even removing the API call and simple returning an object in it's place renders the same result.
Not sure the best way around this to actually return the object?
Any help would be greatly appreciated.
I don't think there is a way to get initial data to useState asynchronously, at least not yet.
React is not waiting for your data to arrive, the function will keep on running to completion while your async operation is queued (on the event loop side).
The current idiomatic way is to fetch the data in an effect and update the state.
useEffect(() => {
getData(someParam).then(data => setState(data))
}, [someParam])
You can read more about it in the DOCS
This isn't something React's built-in hooks support.
You need to build or import a custom hook.
Want short and simple? Try this:
const addressDetails = useAsyncFunctionResult(retrieveDetails);
and add this in hooks/useAsyncFunctionResults.js
function useAsyncFunctionResult(asyncFunction, dependencies = []) {
const [result, setResult] = React.useState();
React.useEffect(() => {
let mounted = true;
asyncFunction().then((data) => mounted && setResult(data))
return () => { mounted = false; }
}, dependencies]);
return result;
}
Here the retrieveDetails function (the one from the question) will start executing (the ().then bit above) when the hook is first called. The result is kept until unmount. And we get no errors about changing component state after unmounting.
If you later want to add caching, use existing hooks instead if making your own.
There's no official useAsync in React, and likely never will be, because if your Promise did a request and the requesting component did unmount before the promise resolves, then best practice is to cancel which differs case by case.
const retrieveDetails = async () => {
const addr = addressDetails[currentAddress];
const { addressLookup } = addr;
const key = process.env.API_KEY;
const query = `?Key=${key}&Id=${addressLookup}`;
const addrDetails = await Promise.resolve(new AddrService().getAddressDetails(query))
return addrDetails;
}
**try this once changed the function a bit**

How do I access the latest value of state using useSelector after dispatching an action in redux?

I have a screen in a React-Native project which essentially just renders a loading icon whilst fetching data from the server, before then taking the user to the main screen. The first function getPrivateKey() will return the private key and store it using redux in the state, and the next function connectWithKey() will then use that key to connect.
The issue I'm facing is that when connectWithkey() runs, it's using the initial, empty value of the private key, not the updated value. Here's the code, and apologies if I'm being stupid it's been a long day :(
export default DataLoader = props => {
//private key - this should, in theory, update after getPrivateKey()
const privateKey = useSelector(({ main }) => main.privateKey);
const dispatch = useDispatch();
useEffect(() => {
const configure = async () => {
//this will update the private key
await getPrivateKey();
//this should use the new private key from useSelector, but instead is using the initialised empty object
await connectWithKey();
props.navigation.navigate('MainScreen');
};
configure();
}, []);
//.... more code below....
I've tried adding privateKey into the array dependencies which just caused an infinite loop, and I've checked that the value has updated in the redux store - so I'm a bit lost! In essence, it appears that the useSelector hook isn't getting a fresh value. Any help would be very much appreciated 😊 Thanks!
EDIT - added more code upon request 😊
const getPrivateKey = async () => {
const privKey = await fetchKeyFromServer();
dispatch({
type: 'UPDATE',
value: privKey
});
};
const connectWithkey = async () => {
//the privateKey here should be the updated value from useSelector
await connectToServer(privateKey)
};
Looks like your getPrivateKey function is a thunk, but you are not dispatching it ? And there is nothing stopping you from returning values from thunks.
const getPrivateKey = async (dispatch) => {
const privKey = await fetchKeyFromServer();
dispatch({
type: 'UPDATE',
value: privKey
});
return privKey // return the key here to whoever wants to use the value immediately.
};
Then in your useEffect in the component you can use the return value easily :)
useEffect(() => {
const configure = async () => {
//make sure you 'dispatch' this thunk
const key = await dispatch(getPrivateKey());
// pass the key
await dispatch(connectWithKey(key));
...
};
....
}, []);
The code above assumes that the connectWithKey is a thunk too. If so, you can design the thunk in a way that it either uses the passed value or reads it from the redux store.
const connectWithkey = (privateKey: passedPrivateKey) = async (dispatch, getState) => {
const state = getState();
let privateKey = state.whatever.the.path.is.to.privateKey;
// use the passed private key if it is present.
if (passedPrivateKey) {
privateKey = passedPrivateKey;
}
await connectToServer(privateKey)
};
I have used this approach several times in my app. This way you do not need to rely on the state in the selector. And should you choose to rely on that state, the dependencies of your useEffect should update accordingly. Right now it is an empty array, and that is why the effect doesn't run again on any state changes (it is acting like the componentDidMount lifecycle function).
const privateKey = useSelector(({ main }) => main.privateKey);
useEffect(() => {
await getPrivateKey();
if (privateKey) {
await connectWithKey();
}
}, [privateKey]);
This way your hook re-runs everytime privateKey state changes. You might need to have some sort of condition for your connectWithKey thunk though, so that it doesn't run if the key is null.

Setting a useEffect hook's dependency within`useEffect` without triggering useEffect

Edit: It just occurred to me that there's likely no need to reset the variable within the useEffect hook. In fact, stateTheCausesUseEffectToBeInvoked's actual value is likely inconsequential. It is, for all intents and purposes, simply a way of triggering useEffect.
Let's say I have a functional React component whose state I initialize using the useEffect hook. I make a call to a service. I retrieve some data. I commit that data to state. Cool. Now, let's say I, at a later time, interact with the same service, except that this time, rather than simply retrieving a list of results, I CREATE or DELETE a single result item, thus modifying the entire result set. I now wish to retrieve an updated copy of the list of data I retrieved earlier. At this point, I'd like to again trigger the useEffect hook I used to initialize my component's state, because I want to re-render the list, this time accounting for the newly-created result item.
​
const myComponent = () => {
const [items, setItems] = ([])
useEffect(() => {
const getSomeData = async () => {
try {
const response = await callToSomeService()
setItems(response.data)
setStateThatCausesUseEffectToBeInvoked(false)
} catch (error) {
// Handle error
console.log(error)
}
}
}, [stateThatCausesUseEffectToBeInvoked])
const createNewItem = async () => {
try {
const response = await callToSomeService()
setStateThatCausesUseEffectToBeInvoked(true)
} catch (error) {
// Handle error
console.log(error)
}
}
}
​
I hope the above makes sense.
​
The thing is that I want to reset stateThatCausesUseEffectToBeInvoked to false WITHOUT forcing a re-render. (Currently, I end up calling the service twice--once for win stateThatCausesUseEffectToBeInvoked is set to true then again when it is reset to false within the context of the useEffect hook. This variable exists solely for the purpose of triggering useEffect and sparing me the need to elsewhere make the selfsame service request that I make within useEffect.
​
Does anyone know how this might be accomplished?
There are a few things you could do to achieve a behavior similar to what you described:
Change stateThatCausesUseEffectToBeInvoked to a number
If you change stateThatCausesUseEffectToBeInvoked to a number, you don't need to reset it after use and can just keep incrementing it to trigger the effect.
useEffect(() => {
// ...
}, [stateThatCausesUseEffectToBeInvoked]);
setStateThatCausesUseEffectToBeInvoked(n => n+1); // Trigger useEffect
Add a condition to the useEffect
Instead of actually changing any logic outside, you could just adjust your useEffect-body to only run if stateThatCausesUseEffectToBeInvoked is true.
This will still trigger the useEffect but jump right out and not cause any unnecessary requests or rerenders.
useEffect(() => {
if (stateThatCausesUseEffectToBeInvoked === true) {
// ...
}
}, [stateThatCausesUseEffectToBeInvoked]);
Assuming that 1) by const [items, setItems] = ([]) you mean const [items, setItems] = useState([]), and 2) that you simply want to reflect the latest data after a call to the API:
When the state of the component is updated, it re-renders on it's own. No need for stateThatCausesUseEffectToBeInvoked:
const myComponent = () => {
const [ items, setItems ] = useState( [] )
const getSomeData = async () => {
try {
const response = await callToSomeService1()
// When response (data) is received, state is updated (setItems)
// When state is updated, the component re-renders on its own
setItems( response.data )
} catch ( error ) {
console.log( error )
}
}
useEffect( () => {
// Call the GET function once ititially, to populate the state (items)
getSomeData()
// use [] to run this only on component mount (initially)
}, [] )
const createNewItem = async () => {
try {
const response = await callToSomeService2()
// Call the POST function to create the item
// When response is received (e.g. is OK), call the GET function
// to ask for all items again.
getSomeData()
} catch ( error ) {
console.log( error )
}
} }
However, instead of getting all items after every action, you could change your array locally, so if the create (POST) response.data is the newly created item, you can add it to items (create a new array that includes it).

Categories