I have a function that refreshes the data of my component when the function is called. At this moment it only works for one component at a time. But I want to refresh two components at once. This is my refresh function:
fetchDataByName = name => {
const { retrievedData } = this.state;
const { fetcher } = this.props;
const fetch = _.find(fetcher, { name });
if (typeof fetch === "undefined") {
throw new Error(`Fetch with ${name} cannot be found in fetcher`);
}
this.fetchData(fetch, (error, data) => {
retrievedData[name] = data;
this._isMounted && this.setState({ retrievedData });
});
};
My function is called like this:
refresh("meetingTypes");
As it it passed as props to my component:
return (
<Component
{...retrievedData}
{...componentProps}
refresh={this.fetchDataByName}
/>
);
I tried passing multiple component names as an array like this:
const args = ['meetingTypes', 'exampleMeetingTypes'];
refresh(args);
And then check in my fetchDataByName function if name is an array and loop through the array to fetch the data. But then the function is still executed after each other instead of at the same time. So my question is:
What would be the best way to implement this that it seems like the
function is executed at once instead of first refreshing meetingTypes
and then exampleMeetingTypes?
Should I use async/await or are there better options?
The fetchData function:
fetchData = (fetch, callback) => {
const { componentProps } = this.props;
let { route, params = [] } = fetch;
let fetchData = true;
// if fetcher url contains params and the param can be found
// in the component props, they should be replaced.
_.each(params, param => {
if (componentProps[param]) {
route = route.replace(`:${param}`, componentProps[param]);
} else {
fetchData = false; // don't fetch data for this entry as the params are not given
}
});
if (fetchData) {
axios
.get(route)
.then(({ data }) => {
if (this.isMounted) {
callback(null, data);
}
})
.catch(error => {
if (error.response.status == 403) {
this._isMounted && this.setState({ errorCode: 403 });
setMessage({
text: "Unauthorized",
type: "error"
});
}
if (error.response.status == 401) {
this._isMounted && this.setState({ errorCode: 401 });
window.location.href = "/login";
}
if (error.response.status != 403) {
console.error("Your backend is failing.", error);
}
callback(error, null);
});
} else {
callback(null, null);
}
};
I assume fetchData works asynchronously (ajax or similar). To refresh two aspects of the data in parallel, simply make two calls instead of one:
refresh("meetingTypes");
refresh("exampleMeetingTypes");
The two ajax calls or whatever will run in parallel, each updating the component when it finishes. But: See the "Side Note" below, there's a problem with fetchDataByName.
If you want to avoid updating the component twice, you'll have to update fetchDataByName to either accept multiple names or to return a promise of the result (or similar) rather than updating the component directly, so the caller can do multiple calls and wait for both results before doing the update.
Side note: This aspect of fetchDataByName looks suspect:
fetchDataByName = name => {
const { retrievedData } = this.state; // <=============================
const { fetcher } = this.props;
const fetch = _.find(fetcher, { name });
if (typeof fetch === "undefined") {
throw new Error(`Fetch with ${name} cannot be found in fetcher`);
}
this.fetchData(fetch, (error, data) => {
retrievedData[name] = data; // <=============================
this._isMounted && this.setState({ retrievedData });
});
};
Two problems with that:
It updates an object stored in your state directly, which is something you must never do with React.
It replaces the entire retrievedData object with one that may well be stale.
Instead:
fetchDataByName = name => {
// *** No `retrievedData` here
const { fetcher } = this.props;
const fetch = _.find(fetcher, { name });
if (typeof fetch === "undefined") {
throw new Error(`Fetch with ${name} cannot be found in fetcher`);
}
this.fetchData(fetch, (error, data) => {
if (this._isMounted) { // ***
this.setState(({retrievedData}) => ( // ***
{ retrievedData: {...retrievedData, [name]: data} } // ***
); // ***
} // ***
});
};
That removes the in-place mutation of the object with spread, and uses an up-to-date version of retrievedData by using the callback version of setState.
Related
I am trying to get data from local memory using asyncStorage but there is one issue
useEffect( async () => {
try {
if(activemanagegroup !== null) {
var groupValue = JSON.stringify(activemanagegroup)
await AsyncStorage.setItem('managementGroup', groupValue)
}
var listValue = JSON.stringify(list)
await AsyncStorage.setItem('selectedList', listValue)
} catch (e) {
console.log('Failed to save data')
}
},[activemanagegroup, list])
useEffect(() => {
async function getData() {
try {
const managementGroupValue = await AsyncStorage.getItem('managementGroup')
const managedUsersList = await AsyncStorage.getItem('selectedList')
const activeManagementGroupSelected = managementGroupValue != null ? JSON.parse(managementGroupValue) : null
const activeList = managedUsersList != null ? JSON.parse(managedUsersList) : null
setActiveManagementGroup(activeManagementGroupSelected)
setNewList(activeList)
} catch (error) {
console.log('error getting data', error)
}
}
getData()
},[activemanagegroup])
the problem is selectedList updates a second later after managementGroup and due to that I end up getting old selectedList. How I can delay the call and make sure I get updated selectedList ?
Note: I am storing both these values once user presses a button.
I wouldn't recommend using AsyncStorage to retrieve the data more than once; once you have the data loaded initially, you should use React's built-in state management solutions to store the data instead of re-reading it from AsyncStorage.
Thus, I'd move your getItem calls to a separate useEffect that only runs once and updates the local React state:
const [managementGroupValue, setManagementGroupValue] = useState(null)
const [managedUsersList, setManagedUsersList] = useState(null)
useEffect(() => {
async function getData() {
try {
setManagementGroupValue(await AsyncStorage.getItem('managementGroup'))
setManagedUsersList(await AsyncStorage.getItem('selectedList'))
} catch (error) {
console.log('error getting data', error)
}
}
getData()
}, [])
and then use the managementGroupValue and managedUsersList variables to refer to that data instead of retrieving it from AsyncStorage each time.
I'm using useSWR to fetch data from client side in nextjs.
What I am doing and trying to achieve
I am using useSWRInfinite for the pagination feature and trying to update comments like state with bound mutate function with optimisticData option since I wanted to refresh the data immediately.(client-side perspective)
-> https://swr.vercel.app/docs/mutation#optimistic-updates and then get a new updated comment from axios and replace it with a previous comment that should be updated.
Expected
The data from useSWRInfinite should be updated right away since I am using optimisticData option until the API call is done and I could've set revalidate option to true but an async function in the mutate returns updated data with the response from axios. I didn't need it.
Actual behaviour
Even though I am passing optimisticData to the mutate, It doesn't update the data immediately. It keeps waiting until The API call is done and then gets updated.
What I've tried
I have tried using just normal useSWR function without the pagination feature and it worked well as I expected.
const { data, error, isValidating, mutate, size, setSize } = useSWRInfinite<CommentType[]>(
(index) => `/api/comment?postId=${postId}¤tPage=${index + 1}`,
fetcher,
{ revalidateFirstPage: false }
);
const likeCommentHandler = async (commentId: string, dislike: boolean) => {
const optimisticData = data?.map((comments) => {
return comments.map((comment) => {
if (comment.id === commentId) {
if (dislike) {
--comment._count.likedBy;
comment.likedByIds = comment.likedByIds.filter(
(likeById) => likeById !== session!.user.id
);
} else {
comment.likedByIds.push(session!.user.id);
++comment._count.likedBy;
}
return { ...comment };
} else {
return { ...comment };
}
});
});
mutate(
async (data) => {
const { data: result } = await axios.post("/api/likeComment", {
commentId: commentId,
userId: session?.user.id,
dislike,
});
const newData = data?.map((comments) => {
return comments.map((comment) => {
if (comment.id === result.comment.id) {
return result.comment;
} else {
return comment;
}
});
});
return newData;
},
{ optimisticData, revalidate: false, populateCache: true }
);
};
I have a function that sends data to the server and uses props and set.... It is the same throughout few components. It gets called when a certain event occurs.
How can I refactor it out of those components into a single place?
I was thinking about using hooks but because it gets triggered by an event I don't think using a hook is a good approach.
async function sendDataToServer(data) {
const url = new URL(buildUrl());
let timeout = setTimeout(() => setPostingState(SendingState.Sending), 250);
try {
const response = props.id
? await axios.put(url, data)
: await axios.post(url, data);
setPostingState(SendingState.Idle);
props.onSaved(props.id ? props.id : response.data, data);
}
catch (error) {
setPostingState(SendingState.Error);
}
clearTimeout(timeout);
}
function handleSubmit(e) { ... sendDataToServer(data); ... }
You can make a curried function:
// helpers.js
export const genSendDataToServerCallback = ({ setState, onSaved, id }) => async (
data
) => {
const url = new URL(buildUrl());
let timeout = setTimeout(() => setState(SendingState.Sending), 250);
try {
const response = await (props.id
? axios.put(url, data)
: axios.post(url, data));
setState(SendingState.Idle);
onSaved(id ? id : response.data, data);
} catch (error) {
setState(SendingState.Error);
}
clearTimeout(timeout);
};
// Usage in some component
import { genSendDataToServerCallback } from './helpers.js'
const sendDataToServer = genSendDataToServerCallback({setter:setPostingState, ...props});
function handleSubmit(e) { sendDataToServer(data); }
// Usage in other component with different setter
const sendDataToServer = genSendDataToServerCallback({setter:setState, ...props});
function handleSubmit(e) { sendDataToServer(data); }
I have a function that registers an effect hook, but it fails because inside the effect I need an object which at the time of running is not defined yet. Through debugging, I've noticed that the object (publicTypeIndex, in this case) is populated after the execution of the async callback.
Here is my code:
export function useNotesList() {
const publicTypeIndex: any = usePublicTypeIndex();
const [notesList, setNotesList] = React.useState<TripleDocument>();
React.useEffect(() => {
if (!publicTypeIndex) {
return;
}
(async () => {
const notesListIndex = publicTypeIndex.findSubject(solid.forClass, schema.TextDigitalDocument);
if (!notesListIndex) {
// If no notes document is listed in the public type index, create one:
const notesList = await initialiseNotesList()
if (notesList == null) {
return;
}
setNotesList(notesList);
return;
} else {
// If the public type index does list a notes document, fetch it:
const notesListUrl = notesListIndex.getRef(solid.instance);
if (typeof notesListUrl !== 'string') {
return;
}
const document = await fetchDocument(notesListUrl);
setNotesList(document);
}
})();
}, [publicTypeIndex])
return notesList;
}
The usePublicTypeIndex function was written as follows:
export async function usePublicTypeIndex() {
const [publicTypeIndex, setPublicTypeIndex] = React.useState<TripleDocument>();
React.useEffect(() => {
fetchPublicTypeIndex().then(fetchedPublicTypeIndex => {
if (fetchedPublicTypeIndex === null) {
console.log("The fetched public type index is null");
return;
}
console.log("Fetched Public Type Index: ");
console.log(fetchedPublicTypeIndex);
setPublicTypeIndex(fetchedPublicTypeIndex);
});
}, []);
return publicTypeIndex;
}
I'd like to find a way to wait for the usePublicTypeIndex() function to return before executing the publicTypeIndex.findSubject(solid.forClass, schema.TextDigitalDocument);code. What's the best way to do it?
Thank you
return in useEffect used as componentWillUnmount, and for cleaning mostly.
In your case, just move the async func into the if block
if (publicTypeIndex) {
(async () => { ... }
}
useEffect will always render at least once when the component mounts.. I ran into same issue as yours before while working on a react weather app.
i used useRef to overcome this issue:
// to stop the useEffect from rendering when the app is mounted for the first time
const initialMount = useRef(true);
useEffect(() => {
// to stop useEffect from rendering at the mounting of the app
if (initialMount.current) {
initialMount.current = false;
} else {
if (publicTypeIndex) {
// do all the stuff you have to do after your object is populated
}
}
},[publicTypeIndex]);
What you did is technically correct, there is no better way to wait for the prop since the callback will run anyway. But you could clean up the code a bit, like:
React.useEffect(() => {
if (publicTypeIndex) {
(async () => {
const notesListIndex = publicTypeIndex.findSubject(
solid.forClass,
schema.TextDigitalDocument
);
if (!notesListIndex) {
// If no notes document is listed in the public type index, create one:
const notesList = await initialiseNotesList();
if (notesList !== null) {
setNotesList(notesList);
}
} else {
// If the public type index does list a notes document, fetch it:
const notesListUrl = notesListIndex.getRef(solid.instance);
if (typeof notesListUrl === 'string') {
const document = await fetchDocument(notesListUrl);
setNotesList(document);
}
}
})();
}
}, [publicTypeIndex]);
Basically there is no point with those returns, you could easily check for required conditions and then invoke the code.
I want to pass in a boolean value as the 2nd argument to my actionCreator which would determine what my middleware dispatches, but how do I give my middleware access to this 2nd argument?
Do I have to dispatch an array or object instead of a promise?
export const fetchPokemon = function (pokemonName, booleanValue) {
return function (dispatch) {
dispatch({type: 'REQUESTING'})
const requestURL = `http://pokeapi.co/api/v2/pokemon/${pokemonName}/`
dispatch(fetch(requestURL))
}
}
Middleware
const fetchPromiseMiddleware = store => next => action => {
if (typeof action.then !== 'function') {
return next(action)
}
...
return response.json()
}).then(function (data) {
if booleanValue {
store.dispatch(receivePokemon(formatPokemonData(data)))
} else {
store.dispatch(fetchPokemonDescription(data.name))
}
})
}
it seems you have answered yourself, the action you dispatch should contain all the relevant data.
The simplest option seem to be to add a property (or properties) to your action, as a Promise is already an object.
export const fetchPokemon = function (pokemonName, booleanValue) {
return function (dispatch) {
dispatch({type: 'REQUESTING'})
const requestURL = `http://pokeapi.co/api/v2/pokemon/${pokemonName}/`
dispatch(Object.assign(fetch(requestURL), {
someNameForYourBooleanParameter: booleanValue
})
}
}
and
const fetchPromiseMiddleware = store => next => action => {
if (typeof action.then !== 'function') {
return next(action)
}
...
return response.json()
}).then(function (data) {
if (action.someNameForYourBooleanParameter) {
store.dispatch(receivePokemon(formatPokemonData(data)))
} else {
store.dispatch(fetchPokemonDescription(data.name))
}
})
}
If you want to continue this path, I'd recommend to put these values under a .payload property to prevent any collision with members of the Promise class
I'd take this approach further to avoid the multiple actions being dispatched for the same logical action:
export const fetchPokemon = function (pokemonName, booleanValue) {
return function (dispatch) {
const requestURL = `http://pokeapi.co/api/v2/pokemon/${pokemonName}/`;
dispatch({
type: 'REQUESTING',
promise: fetch(requestURL),
payload: {
someNameForYourBooleanParameter: booleanValue
}
})
}
}