React Hooks: best practice to get user authenticated - reactjs

I am changing a React app from class based to function based. In the based class the declaration of listeners are in the lifecycle method componentDidMount():
componentDidMount() {
this.getNotes()
Auth.currentAuthenticatedUser().then(user => {
this.setState({user: user});
this.createNoteListener = API.graphql(graphqlOperation(onCreateNote, { owner:this.state.user.username })).subscribe({
next: noteData => {
const newNote = noteData.value.data.onCreateNote
const prevNotes = this.state.notes.filter(note => note.id !== newNote.id)
const updatedNotes = [...prevNotes, newNote]
this.setState({ notes: updatedNotes })
}
})...
To unsubscribe the listener I use the lifecycle method:
componentWillUnmount(){
this.createNoteListener.unsubscribe()
Changing to a function based class the listener declaration is like this:
useEffect(() => {
getNotes()
Auth.currentAuthenticatedUser().then(user => {
const createNoteListener = API.graphql(graphqlOperation(onCreateNote, { owner: user.username })).subscribe({
next: noteData => {
const newNote = noteData.value.data.onCreateNote
setNotes(prevNotes => {
const oldNotes = prevNotes.filter(note => note.id !== newNote.id)
const updatedNotes = [...oldNotes, newNote]
return updatedNotes
})
setNote("")
}
............
return () => {
createNoteListener.unsubscribe() //the error is here
}
I am getting an erro saying: 'createNoteListener' is not defined.
Since I need the authenticated user to create the listener, how/where should I get/set the user before declaring the listener?
Thank you all!

createNoteListener is defined in different scope.
Can you try this?
useEffect(() => {
getNotes()
let createNoteListener = null;
Auth.currentAuthenticatedUser().then(user => {
createNoteListener = API.graphql(graphqlOperation(onCreateNote, { owner: user.username })).subscribe({
next: noteData => {
const newNote = noteData.value.data.onCreateNote
setNotes(prevNotes => {
const oldNotes = prevNotes.filter(note => note.id !== newNote.id)
const updatedNotes = [...oldNotes, newNote]
return updatedNotes
})
setNote("")
}
}
)}
)
return () => {
createNoteListener.unsubscribe() //the error is here
}
}
)

I think you need to provide an extra argument to the useEffect Hook
You can do this
useEffect(() => {
getNotes()
Auth.currentAuthenticatedUser().then(user => {
const createNoteListener = API.graphql(graphqlOperation(onCreateNote, { owner: user.username })).subscribe({
next: noteData => {
const newNote = noteData.value.data.onCreateNote
setNotes(prevNotes => {
const oldNotes = prevNotes.filter(note => note.id !== newNote.id)
const updatedNotes = [...oldNotes, newNote]
return updatedNotes
})
setNote("")
}
............
return () => {
createNoteListener.unsubscribe()
}, []) // provide empty array as second argument
This is because you want to subscribe only once.
Hope it helps

Related

Enable a listener when an application is started or in the foreground

With react-native, AppState component allows to listen if an application is in the foreground (active) or in the background (background).
Its implementation is very simple :
useEffect(() => {
const appStateListener = AppState.addEventListener('change', appState => {
if (appState === 'active') {
console.log('App active')
} else {
console.log('App not active')
}
// !!!!! no console.log() at application startup, only when changing background/foreground !!!!!
})
return () => appStateListener.remove()
}, [])
react-native-mmkv allows to store values locally. It has a listener to detect changes :
useEffect(() => {
const storageListener = storage.addOnValueChangedListener((changedKey) => {
const newValue = storage.getString(changedKey)
console.log(`"${changedKey}" new value: ${newValue}`)
})
return () => storageListener.remove()
}, [])
But I don't know how to use both simultaneously. I would like to activate the react-native-mmkv listener when the application starts (useEffect()) AND when the application is in the foreground.
When the application is closed or in the background, I would like to remove this listener.
I tried this but I know it's not good, and the application crashes when the application goes from the background to the foreground ("Maximum call stack size exceeded").
useEffect(() => {
const enableStorageListener = () => {
return storage.addOnValueChangedListener((changedKey) => {
//...
})
}
let storageListener = enableStorageListener()
const appStateListener = AppState.addEventListener('change', appState => {
if (appState === 'active') {
storageListener = enableStorageListener()
} else {
storageListener.remove()
}
})
return () => {
appStateListener.remove()
storageListener.remove()
}
}, [])
Can you try to keep the AppState in a state
const [appState, setAppState] = useState("");
useEffect(() => {
const appStateListener = AppState.addEventListener("change", (appState) => {
if (appState === "active") {
setAppState("active");
console.log("App active");
} else {
setAppState("deactive");
console.log("App not active");
}
// !!!!! no console.log() at application startup, only when changing background/foreground !!!!!
});
return () => appStateListener.remove();
}, []);
useEffect(() => {
const storageListener = storage.addOnValueChangedListener((changedKey) => {
if (appState === "active") {
const newValue = storage.getString(changedKey);
console.log(`"${changedKey}" new value: ${newValue}`);
}
});
return () => storageListener.remove();
}, []);
To use both components, you can combine their listeners within a single useEffect hook, and conditionally enable/disable the storage listener based on the app state:
useEffect(() => {
let storageListener = null;
const enableStorageListener = () => {
storageListener = storage.addOnValueChangedListener((changedKey) => {
// ...
});
};
const appStateListener = AppState.addEventListener('change', (appState) => {
if (appState === 'active') {
enableStorageListener();
} else if (storageListener) {
storageListener.remove();
storageListener = null;
}
});
if (AppState.currentState === 'active') {
enableStorageListener();
}
return () => {
appStateListener.remove();
if (storageListener) {
storageListener.remove();
}
};
}, []);
This way, the storage listener is enabled when the app is active and disabled when it's in the background. The return statement in the hook is used to cleanup the listeners when the component is unmounted.

Rerendering a component - ReactJS, Axios

After calling canBookSlot I want to update the slotsList I figure i have to make a new Axios request, can i reuse the useEffect whitin the then() method to rerender the component with updated properties or is there any other smart way of doing it without rewriting the Axios request?
useEffect(() => {
Axios.post("http://localhost:3001/api/get/week1/ex").then((response) => {
setSlotsList(response.data);
});
}, []);
let userDetailsString = localStorage.getItem("userDetails");
const userDetailsObj = JSON.parse(userDetailsString);
const canBookSlot = (id) => {
if (userDetailsObj.canBook != 0) {
Axios.post("http://localhost:3001/api/book/week1/ex", {
room: userDetailsObj.room,
id: id.id + 1,
}).then(); // update the slotsList
}
};
EDIT:
The userDetailsObj is an object from another component, It isn't the same object as the ones in slotList how do i go about rerendering userDetailsObj
const updateData = () => {
Axios.post("http://localhost:3001/api/get/week1/ex").then((response) => {
setSlotsList(response.data);
});
}
useEffect(() => {
updateData();
}, []);
let userDetailsString = localStorage.getItem("userDetails");
let userDetailsObj = JSON.parse(userDetailsString);
const canBookSlot = (id) => {
if (userDetailsObj.canBook != 0) { // Always true
Axios.post("http://localhost:3001/api/book/week1/ex", {
room: userDetailsObj.room,
id: id.id + 1,
}).then(() => updateData())
}
};
You can create common function and reuse when you want to call that axios api and update the data.
updateData = () => {
Axios.post("http://localhost:3001/api/get/week1/ex").then((response)
=> {
setSlotsList(response.data);
});
}
useEffect(() => {
updatedData();
}, []);
let userDetailsString = localStorage.getItem("userDetails");
const userDetailsObj = JSON.parse(userDetailsString);
const canBookSlot = (id) => {
if (userDetailsObj.canBook != 0) {
Axios.post("http://localhost:3001/api/book/week1/ex", {
room: userDetailsObj.room,
id: id.id + 1,
}).then(() => updateData()); // update the slotsList
}
};

How to get the current state inside socket.io on callback function

const useChat = () => {
const [messages, setMessages] = useState([]);
const socketRef = useRef();
const { chatId } = useSelector(state => state.chatin)
const { chatList } = useSelector(state => state.chatin)
const dispatch = useDispatch()
useEffect(() => {
socketRef.current = io(socketClient);
socketClient.on('chat', (data) => {
const targetMessage = (messages) => messages.findIndex(item => item.message_number === data.message_number);
console.log('targetMessage', targetMessage)
if (targetMessage !== -1) {
messages[targetMessage].is_hide = true;
}
setMessages((messages) => [...messages, data]);
});
return () => {
socketRef.current.disconnect();
};
}, []);
whenever I got new socket data, I wanna change 'messages' data, but can't access it, because it always shows initial data value.After that I have a question about how can I set it?
You can move the if condition inside setMessages function, this way you will get access to the current state:
socketClient.on('chat', (data) => {
setMessages((messages) => {
const targetMessage = messages.findIndex(item => item.message_number === data.message_number);
if (targetMessage !== -1) {
messages[targetMessage].is_hide = true;
}
return [...messages, data]
});
});

React Hooks: how to wait for the data to be fetched before rendering

I have fetch method in useEffect hook:
export const CardDetails = () => {
const [ card, getCardDetails ] = useState();
const { id } = useParams();
useEffect(() => {
fetch(`http://localhost:3001/cards/${id}`)
.then((res) => res.json())
.then((data) => getCardDetails(data))
}, [id])
return (
<DetailsRow data={card} />
)
}
But then inside DetailsRow component this data is not defined, which means that I render this component before data is fetched. How to solve it properly?
Just don't render it when the data is undefined:
export const CardDetails = () => {
const [card, setCard] = useState();
const { id } = useParams();
useEffect(() => {
fetch(`http://localhost:3001/cards/${id}`)
.then((res) => res.json())
.then((data) => setCard(data));
}, [id]);
if (card === undefined) {
return <>Still loading...</>;
}
return <DetailsRow data={card} />;
};
There are 3 ways to not render component if there aren't any data yet.
{data && <Component data={data} />}
Check if(!data) { return null } before render. This method will prevent All component render until there aren't any data.
Use some <Loading /> component and ternar operator inside JSX. In this case you will be able to render all another parts of component which are not needed data -> {data ? <Component data={data} /> : <Loading>}
If you want to display some default data for user instead of a loading spinner while waiting for server data. Here is a code of a react hook which can fetch data before redering.
import { useEffect, useState } from "react"
var receivedData: any = null
type Listener = (state: boolean, data: any) => void
export type Fetcher = () => Promise<any>
type TopFetch = [
loadingStatus: boolean,
data: any,
]
type AddListener = (cb: Listener) => number
type RemoveListener = (id: number) => void
interface ReturnFromTopFetch {
addListener: AddListener,
removeListener: RemoveListener
}
type StartTopFetch = (fetcher: Fetcher) => ReturnFromTopFetch
export const startTopFetch = function (fetcher: Fetcher) {
let receivedData: any = null
let listener: Listener[] = []
function addListener(cb: Listener): number {
if (receivedData) {
cb(false, receivedData)
return 0
}
else {
listener.push(cb)
console.log("listenre:", listener)
return listener.length - 1
}
}
function removeListener(id: number) {
console.log("before remove listener: ", id)
if (id && id >= 0 && id < listener.length) {
listener.splice(id, 1)
}
}
let res = fetcher()
if (typeof res.then === "undefined") {
receivedData = res
}
else {
fetcher().then(
(data: any) => {
receivedData = data
},
).finally(() => {
listener.forEach((cb) => cb(false, receivedData))
})
}
return { addListener, removeListener }
} as StartTopFetch
export const useTopFetch = (listener: ReturnFromTopFetch): TopFetch => {
const [loadingStatus, setLoadingStatus] = useState(true)
useEffect(() => {
const id = listener.addListener((v: boolean, data: any) => {
setLoadingStatus(v)
receivedData = data
})
console.log("add listener")
return () => listener.removeListener(id)
}, [listener])
return [loadingStatus, receivedData]
}
This is what myself needed and couldn't find some simple library so I took some time to code one. it works great and here is a demo:
import { startTopFetch, useTopFetch } from "./topFetch";
// a fakeFetch
const fakeFetch = async () => {
const p = new Promise<object>((resolve, reject) => {
setTimeout(() => {
resolve({ value: "Data from the server" })
}, 1000)
})
return p
}
//Usage: call startTopFetch before your component function and pass a callback function, callback function type: ()=>Promise<any>
const myTopFetch = startTopFetch(fakeFetch)
export const Demo = () => {
const defaultData = { value: "Default Data" }
//In your component , call useTopFetch and pass the return value from startTopFetch.
const [isloading, dataFromServer] = useTopFetch(myTopFetch)
return <>
{isloading ? (
<div>{defaultData.value}</div>
) : (
<div>{dataFromServer.value}</div>
)}
</>
}
Try this:
export const CardDetails = () => {
const [card, setCard] = useState();
const { id } = useParams();
useEffect(() => {
if (!data) {
fetch(`http://localhost:3001/cards/${id}`)
.then((res) => res.json())
.then((data) => setCard(data))
}
}, [id, data]);
return (
<div>
{data && <DetailsRow data={card} />}
{!data && <p>loading...</p>}
</div>
);
};

HowTo: Unsubscribe from Apollo (subscribeToMore) subscription?

So I have a subscription as follows that I wish to unsubscription cleanup on:
<DeleteItem
id={item.id}
urlReferer={urlReferer}
subscribeToDeleteItems={() =>
subscribeToMore({
....
}
})
}
>Delete This Item</DeleteItem>
DeleteItem.js
const DeleteItem = props => {
const {urlReferer} = props;
useEffect(() => {
props.subscribeToDeleteItems();
},[urlReferer]);
...
}
...
}
How would I do this?
So I achieved cleanup by calling an abortController:
useEffect(() => {
const abortController = new AbortController();
props.subscribeToDeleteItems();
return () => {
abortController.abort();
};
},[urlReferer]);
If your effect returns a function, React will run it when it is time to clean up. SubscribeToMore returns the unsubscribe function.
const subscribeToNewComments = () =>
subscribeToMore({
document: MESSAGES_SUBSCRIPTION,
variables: { channelId },
updateQuery: (prev, { subscriptionData }) => {
if (!subscriptionData) return prev;
return Object.assign({}, prev, {
channelMessages: [
subscriptionData.data.messageAdded,
...prev.channelMessages,
],
});
},
});
React.useEffect(() => {
let unsub = subscribeToNewComments();
return () => {
unsub();
};
}, [channelId]);

Resources