React custom hook, counter keeps getting incremented - javascript

I'm using React with hooks, I'm trying to create a custom hook for interaction Observer
For this feature here: Infinite Scroll in react
Since I want for it to be reused multiple times, I want to use it for posts, commments etc
Here is whta I got so far:
useObserver hook:
import React, { useState, useEffect } from 'react';
const useObserver = ({ element, callback }) => {
const [page, setPage] = useState(1);
console.log('props');
console.log(page);
const options = {
root: null,
rootMargin: '0px',
threshold: 1.0
};
const observerHandler = (entities) => {
console.log('handle observer');
const y = entities[0].boundingClientRect.y;
const target = entities[0];
if (target.isIntersecting) {
setPage((counter) => counter + 1);
}
};
useEffect(() => {
const observer = new IntersectionObserver(observerHandler, options);
if (element.current) {
observer.observe(element.current);
}
});
return [1];
};
export default useObserver;
Parent Component where I use hook:
import React, { useRef, useState, useEffect } from 'react';
import useObserver from './useObserver';
const Posts = ({ posts }) => {
// initiate posts loader
const loader = useRef(null);
const [page] = useObserver({ element: loader });
return (
<div id="post-list">
<h1> Post list </h1>
<div class="test" ></div>
<h1>Show posts</h1>
<div className="loading" ref={loader}>
<h1>Loader</h1>
</div>
</div>
);
};
The problem that I'm having is that state page inside of useObserver component get increment always and gets called muliple time continuously, but it should be called only once when user scrolls till that component

try keeping an array with element in useEffect
useEffect(() => {
const observer = new IntersectionObserver(observerHandler, options);
if (element.current) {
observer.observe(element.current);
}
},[element]); //when you specify an empty array it runs only once, an array with value will run when ever the value changes

Related

ReactJS update a component stored in state array

I'm pretty new to coding so I hope my question won't sound too ridiculous. I've spent days to figure something out but didn't find anything.
So I have a react page with the main component that is my "planning container".
In this component, I have a child component that represents a whole month. it's a table with a line for each people and my team, and a td that represents each day of the month.
This child is stored in a variable array that is stored in the state.
When I click on a button, I create another child (for the next month), that I will push into the array stored in the state.
I have a variable called "refresh" that just setRefresh(!refresh). This variable is passed to the childs components and I put this variable in the useEffect [] that will trigger a re-render.
=> The problem is that it doesn't re-render at all.
So, just for you to know, I really need to be able to "append" a whole month at each click on the button. I really need this "view" to be able to work.
I will paste some code to make you understand the main idea.
here is the parent :
import React, {useEffect, useState} from "react";
import PlanningMonthBuild from "./PlanningMonthBuild";
import './Planning.css';
import "react-datepicker/dist/react-datepicker.css";
const PlanningBuild = () => {
const [startForNextMonth, setStartForNextMonth] = useState(new Date());
const [inputList, setInputList] = useState([]);
const [refresh, setRefresh] = useState(false);
useEffect(() => {
initPlanning();
}, []);
const handleRefresh = () => {
setRefresh(!refresh);
}
const initPlanning = () => {
let date = new Date(startForNextMonth.getFullYear(), startForNextMonth.getMonth(), 1);
let newPlanning = [...inputList];
newPlanning.splice(0, 0,
<PlanningMonthBuild
key={'current' + new Date().toISOString()}
startDate={date}
refresh={refresh}
/>
);
let date2 = new Date(startForNextMonth.getFullYear(), startForNextMonth.getMonth() + 1, 1);
date2.setMonth(date.getMonth() + 1);
setStartForNextMonth(date2);
setInputList(newPlanning);
};
const addOneMonthNext = () => {
setInputList(inputList.concat(
<PlanningMonthBuild
key={new Date().toISOString()}
startDate={startForNextMonth}
refresh={refresh}
/>
));
let date = startForNextMonth;
date.setDate(1);
date.setMonth(date.getMonth() + 1);
setStartForNextMonth(date);
};
return (
<main>
<div className="refresh-div" onClick={handleRefresh}><i className="fa fa-refresh" aria-hidden="true"></i></div>
<button className="myButton" onClick={addOneMonthNext}>Add Next Month</button>
<div id="insideDiv">
{inputList}
</div>
</main>
);
};
export default PlanningBuild;
And here is the child (that build each month) :
import React, {useEffect, useState} from "react";
import useAxiosPrivate from "../hooks/useAxiosPrivate";
import {getFirstDayOfMonth} from "../utils/dates";
const PlanningMonthBuild = ({ startDate, refresh }) => {
const [dateDebut] = useState(startDate);
const [jsListe, setJsListe] = useState([]);
const axiosPrivate = useAxiosPrivate();
useEffect(() => {
let isMounted = true;
const controller = new AbortController();
try {
axiosPrivate.get('api/planning/team?start=' + getFirstDayOfMonth(dateDebut.getFullYear(), dateDebut.getMonth()), {
signal: controller.signal
}).then(response => {
if (isMounted) {
const newListeJS = response.data;
setJsListe(newListeJS);
}
});
} catch (err) {}
return () => {
isMounted = false;
controller.abort();
}
}, [refresh])
return (jsListe.map((day)=> {
///.... building a table(tr,td...)
}))
}
export default PlanningMonthBuild;
So, when I put my child component directly into the parent return, it works when I click on refresh button (if my workmate updates something for example...), it will update my table but if I store my child component in the array in "inputList" state it doesn't...
I hope you can understand what I mean.
Thanks in advance for any help.

How to re-render a custom hook after initial render

I have custom hook named useIsUserSubscribed that checks to see a specific user is subscribed. It returns true if the user is subscribed and false if the user is not subscribed...
import { useState, useEffect } from "react";
import { useSelector } from "react-redux";
import { checkSubscription } from "../services";
// this hook checks if the current user is subscribed to a particular user(publisherId)
function useIsUserSubscribed(publisherId) {
const [userIsSubscribed, setUserIsSubscribed] = useState(null);
const currentUserId = useSelector((state) => state.auth.user?.id);
useEffect(() => {
if (!currentUserId || !publisherId) return;
async function fetchCheckSubscriptionData() {
try {
const res = await checkSubscription(publisherId);
setUserIsSubscribed(true);
} catch (err) {
setUserIsSubscribed(false);
}
}
fetchCheckSubscriptionData();
}, [publisherId, currentUserId]);
return userIsSubscribed;
}
export default useIsUserSubscribed;
...I have a button using this hook that renders text conditionally based on the boolean returned from useIsUserSubscribed...
import React, { useEffect, useState } from "react";
import { add, remove } from "../../services";
import useIsUserSubscribed from "../../hooks/useIsUserSubscribed";
const SubscribeUnsubscribeBtn = ({profilePageUserId}) => {
const userIsSubscribed = useIsUserSubscribed(profilePageUserId);
const onClick = async () => {
if (userIsSubscribed) {
// this is an API Call to the backend
await removeSubscription(profilePageUserId);
} else {
// this is an API Call to the backend
await addSubscription(profilePageUserId);
}
// HOW CAN I RERENDER THE HOOK HERE!!!!?
}
return (
<button type="button" className="sub-edit-unsub-btn bsc-button" onClick={onClick}>
{userIsSubscribed ? 'Subscribed' : 'Unsubscribed'}
</button>
);
}
After onClick I would like to rerender my the useIsUserSubscribed hook So that my button text toggles. Can this be done?
you can not use useEffect in your hook for that purpose try this :
hook :
function useIsUserSubscribed() {
const currentUserId = useSelector((state) => state.auth.user?.id);
const checkUser = useCallback(async (publisherId, setUserIsSubscribed) => {
if (!currentUserId || !publisherId) return;
try {
const res = await checkSubscription(publisherId);
setUserIsSubscribed(true);
} catch (err) {
setUserIsSubscribed(false);
}
}, [currentUserId]);
return {checkUser};
}
export default useIsUserSubscribed;
component :
const SubscribeUnsubscribeBtn = ({profilePageUserId}) => {
const [userIsSubscribed,setUserIsSubscribed]=useState(false);
const { checkUser } = useIsUserSubscribed();
useEffect(()=>{
checkUser(profilePageUserId,setUserIsSubscribed)
},[checkUser,profilePageUserId]);
const onClick = async () => {
if (userIsSubscribed) {
// this is an API Call to the backend
await removeSubscription(profilePageUserId);
} else {
// this is an API Call to the backend
await addSubscription(profilePageUserId);
}
// HOW CAN I RERENDER THE HOOK HERE!!!!?
checkUser(profilePageUserId,setUserIsSubscribed)
}
return (
<button type="button" className="sub-edit-unsub-btn bsc-button" onClick={onClick}>
{userIsSubscribed ? 'Subscribed' : 'Unsubscribed'}
</button>
);
}
you can also add some loading state in your hook and return them too so you can check if process is already done or not
Add a dependece on useIsUserSubscribed's useEffect.
hook :
function useIsUserSubscribed(publisherId) {
const [userIsSubscribed, setUserIsSubscribed] = useState(null);
const currentUserId = useSelector((state) => state.auth.user?.id);
// add refresh dependece
const refresh = useSelector((state) => state.auth.refresh);
useEffect(() => {
...
}, [publisherId, currentUserId, refresh]);
...
}
component :
const onClick = async () => {
...
// HOW CAN I RERENDER THE HOOK HERE!!!!?
// when click, you can dispatch a refresh flag.
dispatch(refreshSubState([]))
}
Expose forceUpdate metheod.
hook :
function useIsUserSubscribed(publisherId) {
const [update, setUpdate] = useState({});
const forceUpdate = () => {
setUpdate({});
}
return {userIsSubscribed, forceUpdate};
}
component :
const {userIsSubscribed, forceUpdate} = useIsUserSubscribed(profilePageUserId);
const onClick = async () => {
...
forceUpdate();
}
Here is another solution by user #bitspook
SubscribeUnsubscribeBtn has a dependency on useIsUserSubscribed, but useIsUserSubscribed don't depend on anything from SubscribeUnsubscribeBtn.
Instead, useIsUserSubscribed is keeping a local state. You have a couple of choices here:
Move the state regarding whetehr user is subscribed or not one level up, since you are using Redux, perhaps in Redux.
Communicate to useIsUserSubscribed that you need to change its internal state.
For 1)
const [userIsSubscribed, setUserIsSubscribed] = useState(null);
move this state to Redux store and use it with useSelector.
For 2), return an array of value and callback from the hook, instead of just the value. It will allow you to communicate from component back into the hook.
In useIsUserSubscribed,
return [userIsSubscribed, setUserIsSubscribed];
Then in onClick, you can call setUserIsSubscribed(false), changing the hook's internal state, and re-rendering your component.

Infinite loop (react firebase firestore reactfire)

I have the following react component that creates a new document ref and then subscribes to it with the useFirestoreDocData hook.
This hook for some reason triggers an infinite rerender loop in the component.
Can anyone see what might cause the issue?
import * as React from 'react';
import { useFirestore, useFirestoreDocData, useUser } from 'reactfire';
import 'firebase/firestore';
import 'firebase/auth';
import { useEffect, useState } from 'react';
import { Link, useHistory } from 'react-router-dom';
interface ICreateGameProps {
}
interface IGameLobbyDoc {
playerOne: string;
playerTwo: string | null;
}
const CreateGame: React.FunctionComponent<ICreateGameProps> = (props) => {
const user = useUser();
const gameLobbyDocRef = useFirestore()
.collection('GameLobbies')
.doc()
//This for some reason triggers an infinite loop
const { status, data } = useFirestoreDocData<IGameLobbyDoc>(gameLobbyDocRef);
const [newGameId, setNewGameId] = useState('')
const history = useHistory();
useEffect(() => {
async function createGameLobby() {
const gl: IGameLobbyDoc = {
playerOne: user.data.uid,
playerTwo:null
}
if (user.data.uid) {
const glRef = await gameLobbyDocRef.set(gl)
setNewGameId(gameLobbyDocRef.id)
}
}
createGameLobby()
return () => {
gameLobbyDocRef.delete();
}
}, [])
return <>
<h2>Gameid : {newGameId}</h2>
<p>Waiting for second player to join...</p>
<Link to="/">Go Back</Link>
</>
};
export default CreateGame;
The problem is when you're calling doc() without arguments:
firestore creates new document ref each time with new auto-generated id.
you pass this reference to the useFirestoreDocData that is responsible for creating and observing this document.
useFirestoreDocData makes request to the server to inform about new draft document.
server responds to this request with ok-ish response (no id conflicts, db is accessible, etc...).
created observer updates status of the created document
that triggers rerender (since the document data has updated).
on new rerender .doc() creates new document ref
gives it to the useFirestoreDocData and...
I believe you've got the idea.
To break out of this unfortunate loop we should ensure the .doc() call happens only once on the first render and the ref created by the it doesn't change on each rerender. That's exactly what useRef is for:
...
const gameLobbyDocRef = React.useRef(useFirestore()
.collection('GameLobbies')
.doc())
const { status, data } = useFirestoreDocData<IGameLobbyDoc>(gameLobbyDocRef.current);
...

Which is the right way to detect first render in a react component

I have a scenario where I need to detect the first render of a component. Here I have build a small example. Could someone explain to me what is the correct approach?
Why do most of the people suggest to use a ref instead of a plain state.
https://codesandbox.io/s/condescending-burnell-0ex3x?file=/src/App.js
import React, { useState, useRef, useEffect } from "react";
import "./styles.css";
export default function App() {
const firstRender = useDetectFirstRender();
const [random, setRandom] = useState("123");
useEffect(() => {
if (firstRender) {
console.log("first");
} else {
console.log("second");
}
}, [random]);
return (
<div className="App">
<h1>Random Number is {random}</h1>
<button onClick={() => setRandom(Math.random())}>Change Name</button>
</div>
);
}
//Approach 1
// export function useDetectFirstRender() {
// const firstRender = useRef(true);
// useEffect(() => {
// firstRender.current = false;
// }, []);
// return firstRender.current;
// }
//Approach 2
export function useDetectFirstRender() {
const [firstRender, setFirstRender] = useState(true);
useEffect(() => {
setFirstRender(false);
}, []);
return firstRender;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>
You could create a reusable custom hook for that, based on useRef.
function useFirstRender() {
const ref = useRef(true);
const firstRender = ref.current;
ref.current = false;
return firstRender;
}
you can detect and save it by using useMemo or useCallback hook. but here the most preferable is useMemo as it prevent the same rendering again and again.
const firstRender = useMemo(
() =>console.log('first Render'),
[]
);
here it will render once and save value in the first Render,so you can use this anywhere where you need.
const firstRender = useRef(true);
useEffect(() => {
if (firstRender.current) {
firstRender.current = false;
return;
}
doSomething()
});
The useEffect hook takes a second parameter. This second param is an array of variables that the component will check ensure they've changed before re-rendering. However, if that array is empty, the hook is only called once during initial render. This is similar to the useMemo() trick posted previously.
useEffect(() => doSomethingOnce(), [])
^^

React hooks component callback only has initial state

I am new to react hooks, however I have a problem that I would think is fairly straight forward. Here is my parent component:
import React, { useState, useEffect } from 'react';
import DragAndDrop from '../DragAndDrop';
import Attachment from './Attachment';
import API from '../../services/api';
import '../../styles/components/attachments.scss';
const api = API.create();
const Attachments = ({attachments, type, typeId}) => {
const [attachmentData, setAttachmentData] = useState([]);
useEffect(() => {
setAttachmentData(attachments);
}, [attachments])
function onUpload(files) {
if (typeId) {
api.AddAttachment(type, typeId, files).then(response => {
let newAttachments = response.data.data;
let newAttachmentData = attachmentData;
newAttachmentData = newAttachmentData.concat(newAttachments);
setAttachmentData(newAttachmentData);
});
}
}
return (
<div className="attachments">
<h3 className="attachments-title">Attachments</h3>
<DragAndDrop onUpload={onUpload} />
{attachmentData.map((attachment, index) => (
<Attachment key={index} attachment={attachment} />
))}
</div>
);
}
export default Attachments;
attachments is passed in from the parent component async, which is why I'm using the useEffect function.
This all works fine, and the child Attachment components are rendered when the data is received.
I have a callback onUpload which is called from DragAndDrop component:
import React, { useCallback } from 'react';
import {useDropzone} from 'react-dropzone';
import '../styles/components/dragAndDrop.scss';
const DragAndDrop = ({onUpload}) => {
const onDrop = useCallback(acceptedFiles => {
onUpload(acceptedFiles);
}, [])
const {getRootProps, getInputProps} = useDropzone({onDrop});
return (
<div>
<div {...getRootProps({className: 'dropzone'})}>
<input {...getInputProps()} />
<p>Drag 'n' drop some files here, or click to select files</p>
</div>
</div>
);
};
export default DragAndDrop;
My problem is, when the callback onUpload in the Attachments component is called, attachmentData is the initial value which is an empty array instead of the being populated with the attachments. In my onUpload function, I'm posting the new uploads to my API which then returns them in the same format as the rest of the attachments. I then want to concat these new attachments to the attachmentData. I need attachmentData to have it's filled in value within the callback. Why is the attachmentData the initial state []? How do I get this to work?
The problem is that you're accessing attachmentData in onUpload which becomes stale by the time you use it, so to get the latest attachmentData you can pass a callback function to you updater function setAttachmentData like this:
function onUpload(files) {
if (typeId) {
api.AddAttachment(type, typeId, files).then(response => {
let newAttachments = response.data.data;
setAttachmentData(prevAttachmentData => ([...prevAttachmentData, ...newAttachments]));
});
}
}
If you want to access the attachmentsData inside onUpload, you can do so by creating a ref and then updating that ref whenever attachmentsData changes, that way you won't have to pass a function to setAttachmentsData also:
const [attachmentsData, setAttachmentsData] = React.useState([]);
const attachmentsDataRef = React.useRef(attachmentsData);
// Update ref whenever state changes
useEffect(() => {
attachmentsDataRef.current = attachmentsData;
}, [attachmentsData]);
// Now in onUpload
function onUpload(files) {
// Here you can access attachmentsDataRef.current and you'll get updated state everytime
if (typeId) {
api.AddAttachment(type, typeId, files).then(response => {
let newAttachments = response.data.data;
setAttachmentData([...attachmentsDataRef.current, ...newAttachments]);
});
}
}

Categories