How to deal with nested useEffect? - javascript

Recently started using Hooks and, as cook as they are, they are giving me a bit of a headache.
I have a custom useFetch() hook that deals with fetching data from the API.
I also have a component where I need to use useFetch a few times and the results must be passed from one to another.
E.g.:
const ComponentName = () => {
const { responseUserInfo } = useFetch('/userinfo')
const { responseOrders } = useFetch(`/orders?id=${responseUserInfo.id}`)
const { isOrderRefundable } = useFetch(`/refundable?id={responseOrders.latest.id}`)
return <div>{isOrderRefundable}</div>
}
So, how do I actually "cascade" the hooks without creating 3 intermediate wrappers? Do I have to use HoC?

Your hook could return a callback, that when called does the API call:
const [getUserInfo, { userInfo }] = useFetch('/userinfo');
const [getOrders, { orders }] = useFetch(`/orders`)
const [getOrderRefundable, { isOrderRefundable }] = useFetch(`/refundable`);
useEffect(getUserInfo, []);
useEffect(() => { if(userInfo) getOrders({ id: userInfo.id }); }, [userInfo]);
useEffect(() => { if(orders) getOrderRefundable({ id: /*..*/ }); }, [orders]);
But if you always depend on the whole data being fetched, I'd just use one effect to load them all:
function useAsync(fn, deps) {
const [state, setState] = useState({ loading: true });
useEffect(() => {
setState({ loading: true });
fn().then(result => { setState({ result }); });
}, deps);
return state;
}
// in the component
const { loading, result: { userInfo, orders, isRefundable } } = useAsync(async function() {
const userInfo = await fetch(/*...*/);
const orders = await fetch(/*...*/);
const isRefundable = await fetch(/*...*/);
return { userInfo, orders, isRefundable };
}, []);

Related

firebase data not being updated with use effect

hi i have to refresh my page to see the effect of the person adding an event to the calendar:
my code is
const handleDateClick = async (DateClickArg) => {
if (DateClickArg) {
const title = prompt("Enter title", DateClickArg.dateStr); // allows user to put a title in
// making object
const event = {
title: title ? title : DateClickArg.dateStr,
start: DateClickArg.date,
allDay: true,
};
allEvents.push(event);
const db = fire.firestore();
let currentUserUID = fire.auth().currentUser.uid;
const doc = await fire
.firestore()
.collection("userCalendar")
.doc(currentUserUID)
.get();
db.collection("userCal/" + currentUserUID + "/activities").add({ event });
}
};
and my getuserinfo is:
const getUserInfo = async () => {
let currentUserUID = fire.auth().currentUser.uid;
const qSnap = await fire
.firestore()
.collection("userCal")
.doc(currentUserUID)
.collection("activities")
.get();
const data = [];
data = qSnap.docs.map((d) => ({
id: d.id,
title: d.data().event.title,
start: d.data().event.start.toDate(),
allDay: d.data().event.allDay,
...d.data(),
}));
//setData(data)
console.log(data);
setData([...data]);
};
useEffect(() => {
let mounted = false;
if (!mounted) {
getUserInfo();
}
return () => {
mounted = true;
};
}, []);
where am i going wrong with my use effect? is there a way for the data to update in the browser once its added to firebase? i am using react full calendar
Using get() only returns a point-in-time snapshot of your data. If you want to listen for realtime updates, use .onSnapshot() instead.
You'll also need to make sure you unsubscribe from updates when your component is cleaned up
useEffect(() => {
const currentUserUID = fire.auth().currentUser.uid;
return fire
.firestore()
.collection("userCal")
.doc(currentUserUID)
.collection("activities")
.onSnapshot(({ docs }) => {
setData(
docs.map((doc) => {
const data = doc.data();
return {
id: doc.id,
title: data.event.title,
start: data.event.start.toDate(),
allDay: data.event.allDay,
...data,
};
})
);
});
}, []);
.onShapshot() returns an unsubscribe function so returning that from your effect hook will run it when your component is unmounted.
Assuming your firebase call is ok, there is an error inside your useEffect call. You are setting the mounted variable wrong, it is supposed to be false when your component is destroyed and true after your component is rendered. Also, to avoid unexpected behaviors I highly recommend using the useRef hook to check that.
function Component() {
const isMounted = useRef(false)
useEffect(() => {
isMounted.current = true;
if (isMounted) {
getUserInfo();
}
return () => { isMounted.current = false }
}, []);
...
}
export default Component;

Nextjs: Does getStaticPaths and getStaticProps with useEffect effect SEO?

I'm using getStaticPaths and getStaticProps to retrive data from backend and than using useeffect to set these data to the state (data).
Why am i doing this? I'm doing this because of load more button, so i have to load more content when the button is clicked.
But my seo doesnt depend on the state(data).
So my question is does seo(header) is rendered on server and than other components that use data from the state?
export const getStaticPaths = async () => {
const data = await get();
if (data.status === 'fail' || !data.data.data) {
return {
paths: [],
fallback: true,
};
}
const paths = data.data.data.map((category: any) => {
return {
params: { categoria: category.attributes.name },
};
});
return {
paths: paths,
fallback: true,
};
};
export const getStaticProps = async (context: any) => {
const id = context.params.categoria.split('-').join(' ');
const dataCateriesExist = await getGenericAxios();
if (
dataCateriesExist.status === 'fail' ||
dataCateriesExist.data.data.length === 0
) {
return { notFound: true };
}
const data = await get();
const dataVideo = await get(....)
return { props: { news: data, video: dataVideo }, revalidate: 60 };
};
how i use useEffect:
useEffect(() => {
if (!router.isFallback) {
setNext(props.news?.data?.links?.next?.href || '');
setData(props.news?.data?.data || []);
setIncluded(props.news?.data?.included || []);
setNextVideo(props.video?.data?.links?.next?.href || '');
setDataVideo(props.video?.data?.data || []);
setIncludedVideo(props.video?.data?.included || []);
}
}, [router.isFallback, props]);
and i have by seo component that does not use data from the state, its only text
Also without useEffect if i try to do something like
const [data, setData] = useState(props.news?.data?.data || []);
and than the path is changed with another category i see the same data as the before page

When routing mswjs/data populates the database with new items and removes the previous one, making it inaccessible

I use next-redux-wrapper, MSW, #mswjs/data and redux-toolkit for storing my data in a store as well as mocking API calls and fetching from a mock Database.
I have the following scenario happening to me.
I am on page /content/editor and in the console and terminal, I can see the data was fetched from the mock database and hydrated from getStaticProps of Editor.js. So now IDs 1 to 6 are inside the store accessible.
Now I click on the PLUS icon to create a new project. I fill out the dialog and press "SAVE". a POST request starts, it's pending and then it gets fulfilled. The new project is now in the mock DB as well as in the store, I can see IDs 1 to 7 now.
Since I clicked "SAVE" and the POST request was successful, I am being routed to /content/editor/7 to view the newly created project.
Now I am on Page [id].js, which also fetched data from the mock DB and then it gets stored and hydrated into the redux store. The idea is, it takes the previous store's state and spreads it into the store, with the new data (if there are any).
Now the ID 7 no longer exists. And IDs 1 to 6 also don't exist anymore, instead, I can see in the console and terminal that IDs 8 to 13 were created, and the previous ones are no more.
Obviously, this is not great. When I create a new project and then switch the route, I should be able to access the newly created project as well as the previously created ones. But instead, they all get overwritten.
It either has something to do with the next-redux-wrapper or MSW, but I am not sure how to make it work. I need help with it. I will post some code now:
Code
getStaticProps
// path example: /content/editor
// Editor.js
export const getStaticProps = wrapper.getStaticProps(
(store) =>
async ({ locale }) => {
const [translation] = await Promise.all([
serverSideTranslations(locale, ['editor', 'common', 'thesis']),
store.dispatch(fetchProjects()),
store.dispatch(fetchBuildingBlocks()),
]);
return {
props: {
...translation,
},
};
}
);
// path example: /content/editor/2
// [id].js
export const getStaticProps = wrapper.getStaticProps(
(store) =>
async ({ locale, params }) => {
const { id } = params;
const [translation] = await Promise.all([
serverSideTranslations(locale, ['editor', 'common', 'thesis']),
store.dispatch(fetchProjects()),
// store.dispatch(fetchProjectById(id)), // issue: fetching by ID returns null
store.dispatch(fetchBuildingBlocks()),
]);
return {
props: {
...translation,
id,
},
};
}
);
Mock Database
Factory
I am going to shorten the code to the relevant bits. I will remove properties for a project, as well es helper functions to generate data.
const asscendingId = (() => {
let id = 1;
return () => id++;
})();
const isDevelopment =
process.env.NODE_ENV === 'development' || process.env.STORYBOOK || false;
export const projectFactory = () => {
return {
id: primaryKey(isDevelopment ? asscendingId : nanoid),
name: String,
// ... other properties
}
};
export const createProject = (data) => {
return {
name: data.name,
createdAt: getUnixTime(new Date()),
...data,
};
};
/**
* Create initial set of tasks
*/
export function generateMockProjects(amount) {
const projects = [];
for (let i = amount; i >= 0; i--) {
const project = createProject({
name: faker.lorem.sentence(faker.datatype.number({ min: 1, max: 5 })),
dueDate: date(),
fontFamily: getRandomFontFamily(),
pageMargins: getRandomPageMargins(),
textAlign: getRandomTextAlign(),
pageNumberPosition: getRandomPageNumberPosition(),
...createWordsCounter(),
});
projects.push(project);
}
return projects;
}
API Handler
I will shorten this one to GET and POST requests only.
import { db } from '../../db';
export const projectsHandlers = (delay = 0) => {
return [
rest.get('https://my.backend/mock/projects', getAllProjects(delay)),
rest.get('https://my.backend/mock/projects/:id', getProjectById(delay)),
rest.get('https://my.backend/mock/projectsNames', getProjectsNames(delay)),
rest.get(
'https://my.backend/mock/projects/name/:id',
getProjectsNamesById(delay)
),
rest.post('https://my.backend/mock/projects', postProject(delay)),
rest.patch(
'https://my.backend/mock/projects/:id',
updateProjectById(delay)
),
];
};
function getAllProjects(delay) {
return (request, response, context) => {
const projects = db.project.getAll();
return response(context.delay(delay), context.json(projects));
};
}
function postProject(delay) {
return (request, response, context) => {
const { body } = request;
if (body.content === 'error') {
return response(
context.delay(delay),
context.status(500),
context.json('Server error saving this project')
);
}
const now = getUnixTime(new Date());
const project = db.project.create({
...body,
createdAt: now,
maxWords: 10_000,
minWords: 7000,
targetWords: 8500,
potentialWords: 1500,
currentWords: 0,
});
return response(context.delay(delay), context.json(project));
};
}
// all handlers
import { buildingBlocksHandlers } from './api/buildingblocks';
import { checklistHandlers } from './api/checklist';
import { paragraphsHandlers } from './api/paragraphs';
import { projectsHandlers } from './api/projects';
import { tasksHandlers } from './api/tasks';
const ARTIFICIAL_DELAY_MS = 2000;
export const handlers = [
...tasksHandlers(ARTIFICIAL_DELAY_MS),
...checklistHandlers(ARTIFICIAL_DELAY_MS),
...projectsHandlers(ARTIFICIAL_DELAY_MS),
...buildingBlocksHandlers(ARTIFICIAL_DELAY_MS),
...paragraphsHandlers(ARTIFICIAL_DELAY_MS),
];
// database
import { factory } from '#mswjs/data';
import {
buildingBlockFactory,
generateMockBuildingBlocks,
} from './factory/buildingblocks.factory';
import {
checklistFactory,
generateMockChecklist,
} from './factory/checklist.factory';
import { paragraphFactory } from './factory/paragraph.factory';
import {
projectFactory,
generateMockProjects,
} from './factory/project.factory';
import { taskFactory, generateMockTasks } from './factory/task.factory';
export const db = factory({
task: taskFactory(),
checklist: checklistFactory(),
project: projectFactory(),
buildingBlock: buildingBlockFactory(),
paragraph: paragraphFactory(),
});
generateMockProjects(5).map((project) => db.project.create(project));
const projectIds = db.project.getAll().map((project) => project.id);
generateMockTasks(20, projectIds).map((task) => db.task.create(task));
generateMockBuildingBlocks(10, projectIds).map((block) =>
db.buildingBlock.create(block)
);
const taskIds = db.task.getAll().map((task) => task.id);
generateMockChecklist(20, taskIds).map((item) => db.checklist.create(item));
Project Slice
I will shorten this one as well to the relevant snippets.
// projects.slice.js
import {
createAsyncThunk,
createEntityAdapter,
createSelector,
createSlice,
current,
} from '#reduxjs/toolkit';
import { client } from 'mocks/client';
import { HYDRATE } from 'next-redux-wrapper';
const projectsAdapter = createEntityAdapter();
const initialState = projectsAdapter.getInitialState({
status: 'idle',
filter: { type: null, value: null },
statuses: {},
});
export const fetchProjects = createAsyncThunk(
'projects/fetchProjects',
async () => {
const response = await client.get('https://my.backend/mock/projects');
return response.data;
}
);
export const saveNewProject = createAsyncThunk(
'projects/saveNewProject',
async (data) => {
const response = await client.post('https://my.backend/mock/projects', {
...data,
});
return response.data;
}
);
export const projectSlice = createSlice({
name: 'projects',
initialState,
reducers: {
// irrelevant reducers....
},
extraReducers: (builder) => {
builder
.addCase(HYDRATE, (state, action) => {
// eslint-disable-next-line no-console
console.log('HYDRATE', action.payload);
const statuses = Object.fromEntries(
action.payload.projects.ids.map((id) => [id, 'idle'])
);
return {
...state,
...action.payload.projects,
statuses,
};
})
.addCase(fetchProjects.pending, (state, action) => {
state.status = 'loading';
})
.addCase(fetchProjects.fulfilled, (state, action) => {
projectsAdapter.addMany(state, action.payload);
state.status = 'idle';
action.payload.forEach((item) => {
state.statuses[item.id] = 'idle';
});
})
.addCase(saveNewProject.pending, (state, action) => {
console.log('SAVE NEW PROJECT PENDING', action);
})
.addCase(saveNewProject.fulfilled, (state, action) => {
projectsAdapter.addOne(state, action.payload);
console.group('SAVE NEW PROJECT FULFILLED');
console.log(current(state));
console.log(action);
console.groupEnd();
state.statuses[action.payload.id] = 'idle';
})
// other irrelevant reducers...
},
});
This should be all the relevant code. If you have questions, please ask them and I will try to answer them.
I have changed how the state gets hydrated, so I turned this code:
.addCase(HYDRATE, (state, action) => {
// eslint-disable-next-line no-console
console.log('HYDRATE', action.payload);
const statuses = Object.fromEntries(
action.payload.projects.ids.map((id) => [id, 'idle'])
);
return {
...state,
...action.payload.projects,
statuses,
};
})
Into this code:
.addCase(HYDRATE, (state, action) => {
// eslint-disable-next-line no-console
console.group('HYDRATE', action.payload);
const statuses = Object.fromEntries(
action.payload.projects.ids.map((id) => [id, 'idle'])
);
state.statuses = { ...state.statuses, ...statuses };
projectsAdapter.upsertMany(state, action.payload.projects.entities);
})
I used the adapter to upsert all entries.

why componentdidmount called two times

I have React Component in componentDidMount fetch data from the server. The issue is componentDidMount called twice also the API called twice. I have a view increment API like youtube video views increment twice in the database because of twice API calling.
class SingleVideoPlay extends React.Component {
constructor(props) {
super(props);
this.player = React.createRef();
}
state = {
autoPlay: true,
relatedVideos: [],
video: null,
user: null,
comments: [],
commentInput: {
value: '',
touch: false,
error: false
},
following: false,
tab: 'comments'
};
_Mounted = false;
componentDidMount() {
this._Mounted = true;
if (this._Mounted) {
const videoId = this.props.match.params.id;
this.getVideoDetails(videoId);
}
}
componentWillUnmount() {
this._Mounted = false;
try {
clearInterval(this.state.videoInterval);
this.props.videoEditUrl('');
} catch (error) {}
}
captureVideoTime = async () => {
const { video } = this.state;
const result = await updateWatchTime({
id: video._id,
time: 1
});
if (result.status === 200) {
const updateVideo = {
...video,
secondsWatched: video.secondsWatched + 1
};
this.setState({ video: updateVideo });
}
};
videoEnded = () => {
clearInterval(this.state.videoInterval);
};
videoPause = () => {
clearInterval(this.state.videoInterval);
};
loadVideo = () => {
clearInterval(this.state.videoInterval);
};
playingVideo = () => {
const interval = setInterval(this.captureVideoTime, 1000);
this.setState({ videoInterval: interval });
};
getVideoDetails = async (videoId) => {
const video = await getVideo(videoId);
if (video.status === 200) {
let response = video.data;
if (this.props.userId)
if (response.user._id === this.props.userId._id)
this.props.videoEditUrl(`/video/edit/${response.media._id}`);
this.setState({
relatedVideos: response.videos.docs,
video: response.media,
user: response.user
});
this.checkIsFollowing();
this.updateVideoStat(response.media._id);
}
};
updateVideoStat = async (id) => videoView(id);
checkIsFollowing = async () => {
const { userId } = this.props;
const { video } = this.state;
if (userId && video) {
const response = await isFollow({
follower: userId._id,
following: video._id
});
if (response) {
this.setState({ following: response.following });
}
}
};
addOrRemoveFollowing = async () => {
this.checkIsFollowing();
const { following, video } = this.state;
const { userId } = this.props;
if (userId) {
if (following) {
const response = await removeFollow({
follower: userId._id,
following: video._id
});
this.setState({ following: false });
} else {
const response = await addFollow({
follower: userId._id,
following: video._id
});
this.setState({ following: true });
}
}
};
submitCommentHandler = async (event) => {
const { userId } = this.props;
event.preventDefault();
if (userId) {
const result = await saveComment({
mediaId: this.state.video._id,
parentId: '0',
userID: userId._id,
userName: userId.username,
comment: this.state.commentInput.value
});
console.log(result);
if (result.status === 200) {
this.getVideoComments();
this.setState({ commentInput: { value: '', touch: false, error: false } });
}
}
};
render() {
const { autoPlay, relatedVideos, video, user, comments, commentInput, following, tab } = this.state;
const { userId } = this.props;
return (
<div className="container-fluid">
some coponents
</div>
);
}
}
const mapStateToProps = (state) => ({
userId: state.auth.user
});
export default connect(mapStateToProps, { videoEditUrl })(SingleVideoPlay);
I don't know why componentDidMount called two times alse it shows memmory lecage issue.
How to Fix it.
Multiple componentDidMount calls may be caused by using <React.StrictMode> around your component. After removing it double calls are gone.
This is intended behavior to help detect unexpected side effects. You can read more about it in the docs. It happens only in development environment, while in production componentDidMount is called only once even with <React.StrictMode>.
This was tested with React 18.1.0
I think the issue exists on the parent component that used SingleVideoPlay component. Probably that parent component caused SingleVideoPlay component rendered more than once.
Also, there is an issue on your code.
componentDidMount() {
this._Mounted = true;
if (this._Mounted) {
const videoId = this.props.match.params.id;
this.getVideoDetails(videoId);
}
}
Here, no need to check if this._Mounted, because it will always be true.
1.Install jQuery by
npm i jquery
import $ from 'jquery'
create your function or jwuery code after the export command or put at the end of the file

Using reducer state inside useEffect

Hello All 👋🏻 I have a question about our favorite Hooks API!
What am I trying to do?
I am trying to fetch photos from some remote system. I store the blob urls for these photos in my reducer state keyed by an id.
I have a helper function wrapped in the memoized version returned by the useCallback hook. This function is called in the useEffect I have defined.
The Problem ⚠️
My callback a.k.a the helper function depends on part of the reducer state. Which is updated every time a photo is fetched. This causes the component to run the effect in useEffect again and thus causing an infinite loop.
component renders --> useEffect runs ---> `fetchPhotos` runs --> after 1st photo, reducer state is updated --> component updates because `useSelector`'s value changes ---> runs `fetchPhotos` again ---> infinite
const FormViewerContainer = (props) => {
const { completedForm, classes } = props;
const [error, setError] = useState(null);
const dispatch = useDispatch();
const photosState = useSelector(state => state.root.photos);
// helper function which fetches photos and updates the reducer state by dispatching actions
const fetchFormPhotos = React.useCallback(async () => {
try {
if (!completedForm) return;
const { photos: reducerPhotos, loadingPhotoIds } = photosState;
const { photos: completedFormPhotos } = completedForm;
const photoIds = Object.keys(completedFormPhotos || {});
// only fetch photos which aren't in reducer state yet
const photoIdsToFetch = photoIds.filter((pId) => {
const photo = reducerPhotos[pId] || {};
return !loadingPhotoIds.includes(pId) && !photo.blobUrl;
});
dispatch({
type: SET_LOADING_PHOTO_IDS,
payload: { photoIds: photoIdsToFetch } });
if (photoIdsToFetch.length <= 0) {
return;
}
photoIdsToFetch.forEach(async (photoId) => {
if (loadingPhotoIds.includes(photoIds)) return;
dispatch(fetchCompletedFormPhoto({ photoId }));
const thumbnailSize = {
width: 300,
height: 300,
};
const response = await fetchCompletedFormImages(
cformid,
fileId,
thumbnailSize,
)
if (response.status !== 200) {
dispatch(fetchCompletedFormPhotoRollback({ photoId }));
return;
}
const blob = await response.blob();
const blobUrl = URL.createObjectURL(blob);
dispatch(fetchCompletedFormPhotoSuccess({
photoId,
blobUrl,
}));
});
} catch (err) {
setError('Error fetching photos. Please try again.');
}
}, [completedForm, dispatch, photosState]);
// call the fetch form photos function
useEffect(() => {
fetchFormPhotos();
}, [fetchFormPhotos]);
...
...
}
What have I tried?
I found an alternative way to fetch photos a.k.a by dispatching an action and using a worker saga to do all the fetching. This removes all the need for the helper in the component and thus no useCallback and thus no re-renders. The useEffect then only depends on the dispatch which is fine.
Question ?
I am struggling with the mental modal of using the hooks API. I see the obvious problem, but I am not sure how could this be done without using redux middlewares like thunks and sagas.
Edit:
reducer function:
export const initialState = {
photos: {},
loadingPhotoIds: [],
};
export default function photosReducer(state = initialState, action) {
const { type, payload } = action;
switch (type) {
case FETCH_COMPLETED_FORM_PHOTO: {
return {
...state,
photos: {
...state.photos,
[payload.photoId]: {
blobUrl: null,
error: false,
},
},
};
}
case FETCH_COMPLETED_FORM_PHOTO_SUCCESS: {
return {
...state,
photos: {
...state.photos,
[payload.photoId]: {
blobUrl: payload.blobUrl,
error: false,
},
},
loadingPhotoIds: state.loadingPhotoIds.filter(
photoId => photoId !== payload.photoId,
),
};
}
case FETCH_COMPLETED_FORM_PHOTO_ROLLBACK: {
return {
...state,
photos: {
...state.photos,
[payload.photoId]: {
blobUrl: null,
error: true,
},
},
loadingPhotoIds: state.loadingPhotoIds.filter(
photoId => photoId !== payload.photoId,
),
};
}
case SET_LOADING_PHOTO_IDS: {
return {
...state,
loadingPhotoIds: payload.photoIds || [],
};
}
default:
return state;
}
}
You could include the photoIdsToFetch calculation logic into your selector function, to reduce the number of renders caused by state change.
const photoIdsToFetch = useSelector(state => {
const { photos: reducerPhotos, loadingPhotoIds } = state.root.photos;
const { photos: completedFormPhotos } = completedForm;
const photoIds = Object.keys(completedFormPhotos || {});
const photoIdsToFetch = photoIds.filter(pId => {
const photo = reducerPhotos[pId] || {};
return !loadingPhotoIds.includes(pId) && !photo.blobUrl;
});
return photoIdsToFetch;
},
equals
);
However the selector function isn't memoized, it returns a new array object every time, thus object equality will not work here. You will need to provide an isEqual method as a second parameter (that will compare two arrays for value equality) so that the selector will return the same object when the ids are the same. You could write your own or deep-equals library for example:
import equal from 'deep-equal';
fetchFormPhotos will depend only on [photoIdsToFetch, dispatch] this way.
I'm not sure about how your reducer functions mutate the state, so this may require some fine tuning. The idea is: select only the state from store that you depend on, that way other parts of the store will not cause re-renders.

Categories

Resources