How to fix React Native Agora asynchronous error - javascript

I was given this Eslint error:
Assignments to the '_engine' variable from inside React Hook useCallback will be lost after each render. To preserve the value over time, store it in a useRef Hook and keep the mutable value in the '.current' property. Otherwise, you can move this variable directly inside useCallback.eslint(react-hooks/exhaustive-deps)
from this code:
const RtcEngineInit = useCallback(async () => {
const {appId} = appInit;
_engine = await RtcEngine.create(appId);
await _engine.enableAudio();
_engine.addListener('UserOffline', (uid: any, reason: any) => {
console.log('UserOffline', uid, reason);
const {peerIds} = appInit;
setAppInit((prevState) => ({
...prevState,
peerIds: peerIds.filter((id) => id !== uid),
}));
});
_engine.addListener(
'JoinChannelSuccess',
(channel: any, uid: any, elapsed: any) => {
console.log('JoinChannelSuccess', channel, uid, elapsed);
setAppInit((prevState) => ({
...prevState,
joinSucceed: true,
}));
},
);
}, []);
React.useEffect(() => {
RtcEngineInit();
}, [RtcEngineInit]);
could someone explain me why this is happening and help me to solve that? thanks.

As the error suggests, You should not have the RTC Engine inside the render loop. All the statements inside the render loop get executed again. To avoid this. You can have the RTC engine inside a useRef hook.
const App: React.FC = () => {
let engine = useRef<RtcEngine | null>(null);
const appid: string = 'APPID';
const channelName: string = 'channel-x';
const [joinSucceed, setJoinSucceed] = useState<boolean>(false);
const [peerIds, setPeerIds] = useState<Array<number>>([]);
useEffect(() => {
/**
* #name init
* #description Function to initialize the Rtc Engine, attach event listeners and actions
*/
async function init() {
if (Platform.OS === 'android') {
//Request required permissions from Android
await requestCameraAndAudioPermission();
}
engine.current = await RtcEngine.create(appid);
engine.current.enableVideo();
engine.current.addListener('UserJoined', (uid: number) => {
//If user joins the channel
setPeerIds((pids) =>
pids.indexOf(uid) === -1 ? [...pids, uid] : pids,
); //add peer ID to state array
});
engine.current.addListener('UserOffline', (uid: number) => {
//If user leaves
setPeerIds((pids) => pids.filter((userId) => userId !== uid)); //remove peer ID from state array
});
engine.current.addListener('JoinChannelSuccess', () => {
//If Local user joins RTC channel
setJoinSucceed(true); //Set state variable to true
});
}
init();
}, []);
return <UI />
};
export default App;
The full example at:
https://github.com/technophilic/Agora-RN-Quickstart/blob/sdk-v3-ts/src/App.tsx

Related

How to update RTK Query cache when Firebase RTDB change event fired (update, write, create, delete)

I am using redux-tookit, rtk-query (for querying other api's and not just Firebase) and Firebase (for authentication and db).
The code below works just fine for retrieving and caching the data but I wish to take advantage of both rtk-query caching as well as Firebase event subscribing, so that when ever a change is made in the DB (from any source even directly in firebase console) the cache is updated.
I have tried both updateQueryCache and invalidateTags but so far I am not able to find an ideal approach that works.
Any assistance in pointing me in the right direction would be greatly appreciated.
// firebase.ts
export const onRead = (
collection: string,
callback: (snapshort: DataSnapshot) => void,
options: ListenOptions = { onlyOnce: false }
) => onValue(ref(db, collection), callback, options);
export async function getCollection<T>(
collection: string,
onlyOnce: boolean = false
): Promise<T> {
let timeout: NodeJS.Timeout;
return new Promise<T>((resolve, reject) => {
timeout = setTimeout(() => reject('Request timed out!'), ASYNC_TIMEOUT);
onRead(collection, (snapshot) => resolve(snapshot.val()), { onlyOnce });
}).finally(() => clearTimeout(timeout));
}
// awards.ts
const awards = dbApi
.enhanceEndpoints({ addTagTypes: ['Themes'] })
.injectEndpoints({
endpoints: (builder) => ({
getThemes: builder.query<ThemeData[], void>({
async queryFn(arg, api) {
try {
const { auth } = api.getState() as RootState;
const programme = auth.user?.unit.guidingProgramme!;
const path = `/themes/${programme}`;
const themes = await getCollection<ThemeData[]>(path, true);
return { data: themes };
} catch (error) {
return { error: error as FirebaseError };
}
},
providesTags: ['Themes'],
keepUnusedDataFor: 1000 * 60
}),
getTheme: builder.query<ThemeData, string | undefined>({
async queryFn(slug, api) {
try {
const initiate = awards.endpoints.getThemes.initiate;
const getThemes = api.dispatch(initiate());
const { data } = (await getThemes) as ApiResponse<ThemeData[]>;
const name = slug
?.split('-')
.map(
(value) =>
value.substring(0, 1).toUpperCase() +
value.substring(1).toLowerCase()
)
.join(' ');
return { data: data?.find((theme) => theme.name === name) };
} catch (error) {
return { error: error as FirebaseError };
}
},
keepUnusedDataFor: 0
})
})
});

How to check input for "Enter" key press

I'm working on a slightly complicated component that basically allows a user to type into an input, and then trigger a search (external API) for that product, the current issue however is that using the "Enter" key press, causes different behaviour and I want to sync up the behaviour of the "Find" button and "Enter". But before that I'm having some trouble on establishing where that check should happen, here's my React component:
export type CcceHook = {
allowForClassification: boolean,
classifyInProgress: boolean,
dataProfileId: string,
embedID: string,
handleCancelClassify: () => void,
handleClassify: (event?: SyntheticEvent<any>) => void,
handleCloseModal: () => void,
handleShowModal: () => void,
isDebugMode: boolean,
resultCode: string | null,
shouldShowModal: boolean,
};
// returns Ccce input fields based on the object form model - used in context provider
const getCcceValues = (object?: FormObjectModel | null) => {
const ccceInput: $Shape<CcceInput> = {};
//WHERE I THINK THE CHECK SHOULD GO (`ccceInput` is an object, with the `ccce.product` containing the users typed entry)
if (!object) {
return {};
}
// ccce input values
const ccceValues = object.attributeCollection.questions.reduce(
(acc, attribute) => {
const fieldEntry = ccceBeInformedFieldMap.get(attribute.key);
if (fieldEntry) {
acc[fieldEntry] = attribute.value;
}
return acc;
},
ccceInput
);
//check for null or empty string and if so hide "find goods button"
const productValueWithoutSpaces =
ccceValues.product && ccceValues.product.replace(/\s+/g, "");
const canClassify =
Object.values(ccceValues).every(Boolean) &&
Boolean(productValueWithoutSpaces);
return { canClassify, ccceValues };
};
export const useCcceEmbed = (
ccceResultAttribute: AttributeType,
onChange: Function
): CcceHook => {
const { object, form } = useFormObjectContext();
const [resultCode, setResultCode] = useState<string | null>(null);
const { canClassify, ccceValues } = getCcceValues(object);
const { handleSubmit } = useFormSubmit();
// data profile id is the 'api key' for 3ce
const dataProfileId = useSelector(
(state) => state.preferences[DATA_PROFILE_ID]
);
// data profile id is the 'api key' for 3ce
const isDebugMode = useSelector((state) => {
const value = state.preferences[CCCE_DEBUG_MODE_PREFERENCE];
try {
return JSON.parse(value);
} catch (error) {
throw new Error(
`3CE configuration error - non-boolean value for ${CCCE_DEBUG_MODE_PREFERENCE}: ${value}`
);
}
});
const [showModal, setShowModal] = useState<boolean>(false);
const handleCloseModal = useCallback(() => setShowModal(false), []);
const handleShowModal = useCallback(() => setShowModal(true), []);
// state value to keep track of a current active classification
const [classifyInProgress, setClassifyInProgress] = useState<boolean>(false);
// handle results from 3ce
const handleResult = useCallback(
(result) => {
if (result?.hsCode) {
onChange(ccceResultAttribute, result.hsCode);
setResultCode(result.hsCode);
setClassifyInProgress(false);
handleSubmit(form);
}
},
[ccceResultAttribute, form, handleSubmit, onChange]
);
const handleCancelClassify = useCallback(() => {
setClassifyInProgress(false);
handleCloseModal();
}, [handleCloseModal]);
// handle 3ce classify (https://github.com/3CETechnologies/embed)
const handleClassify = useCallback(
(event?: SyntheticEvent<any>) => {
if (event) {
event.preventDefault();
}
if (classifyInProgress || !canClassify) {
return;
}
const ccce = window.ccce;
if (!ccceValues || !ccce) {
throw new Error("Unable to classify - no values or not initialised");
}
setClassifyInProgress(true);
const classificationParameters = {
...ccceValues,
...DEFAULT_EMBED_PROPS,
};
ccce.classify(
classificationParameters,
handleResult,
handleCancelClassify
);
},
[
classifyInProgress,
canClassify,
ccceValues,
handleResult,
handleCancelClassify,
]
);
return {
allowForClassification: canClassify && !classifyInProgress,
classifyInProgress,
dataProfileId,
embedID: EMBED_ID,
handleCancelClassify,
handleClassify,
handleCloseModal,
handleShowModal,
isDebugMode,
resultCode,
shouldShowModal: showModal,
};
};
I have added a comment on where I think this logic should be handled (search "//WHERE I THINK..") - however, I'm unsure how to go from knowing the value of the users input, to checking for an enter press, I'm happy just to be able to console.log a user's key press, I should be able to tie up the logic from there, any advice would be really helpful.
TIA!

use async function to get draft inside reducer of useImmerReducer

I have this reducer function that I use for state management of my app.
const initialState = {roles: null};
const reducer = (draft, action) => {
switch (action.type) {
case 'initialize':
//what should i do here????
return;
case 'add':
draft.roles = {...draft.roles, action.role};
return;
case 'remove':
draft.roles = Object.filter(draft.roles, role => role.name != action.role.name);
}
};
const [state, dispatch] = useImmerReducer(reducer, initialState);
to initialize my state I must use an async function that reads something from asyncStorage if it exists, must set draft.roles to it, if not it should be set to a default value.
const initialize = async () => {
try {
let temp = await cache.get();
if (temp == null) {
return defaultRoles;
} else {
return temp;
}
} catch (error) {
console.log('initialization Error: ', error);
return defaultRoles;
}
};
how can I get initilize function returned value inside 'initialize' case? if I use initilize().then(value=>draft.roles=value) I get this error:
TypeError: Proxy has already been revoked. No more operations are allowed to be performed on it
You cannot use asynchronous code inside of a reducer. You need to move that logic outside of the reducer itself. I am using a useEffect hook to trigger the initialize and then dispatching the results to the state.
There are quite a few syntax errors here -- should state.roles be an array or an object?
Here's my attempt to demonstrate how you can do this. Probably you want this as a Context Provider component rather than a hook but the logic is the same.
Javascript:
import { useEffect } from "react";
import { useImmerReducer } from "use-immer";
export const usePersistedReducer = () => {
const initialState = { roles: [], didInitialize: false };
const reducer = (draft, action) => {
switch (action.type) {
case "initialize":
// store all roles & flag as initialized
draft.roles = action.roles;
draft.didInitialize = true;
return;
case "add":
// add one role to the array
draft.roles.push(action.role);
return;
case "remove":
// remove role from the array based on name
draft.roles = draft.roles.filter(
(role) => role.name !== action.role.name
);
return;
}
};
const [state, dispatch] = useImmerReducer(reducer, initialState);
useEffect(() => {
const defaultRoles = []; // ?? where does this come from?
// always returns an array of roles
const retrieveRoles = async () => {
try {
// does this need to be deserialized?
let temp = await cache.get();
// do you want to throw an error if null?
return temp === null ? defaultRoles : temp;
} catch (error) {
console.log("initialization Error: ", error);
return defaultRoles;
}
};
// define the function
const initialize = async() => {
// wait for the roles
const roles = await retrieveRoles();
// then dispatch
dispatch({type: 'initialize', roles});
}
// execute the function
initialize();
}, [dispatch]); // run once on mount - dispatch should not change
// should use another useEffect to push changes
useEffect(() => {
cache.set(state.roles);
}, [state.roles]); // run whenever roles changes
// maybe this should be a context provider instead of a hook
// but this is just an example
return [state, dispatch];
};
Typescript:
import { Draft } from "immer";
import { useEffect } from "react";
import { useImmerReducer } from "use-immer";
interface Role {
name: string;
}
interface State {
roles: Role[];
didInitialize: boolean;
}
type Action =
| {
type: "initialize";
roles: Role[];
}
| {
type: "add" | "remove";
role: Role;
};
// placeholder for the actual
declare const cache: { get(): Role[] | null; set(v: Role[]): void };
export const usePersistedReducer = () => {
const initialState: State = { roles: [], didInitialize: false };
const reducer = (draft: Draft<State>, action: Action) => {
switch (action.type) {
case "initialize":
// store all roles & flag as initialized
draft.roles = action.roles;
draft.didInitialize = true;
return;
case "add":
// add one role to the array
draft.roles.push(action.role);
return;
case "remove":
// remove role from the array based on name
draft.roles = draft.roles.filter(
(role) => role.name !== action.role.name
);
return;
}
};
const [state, dispatch] = useImmerReducer(reducer, initialState);
useEffect(() => {
const defaultRoles: Role[] = []; // ?? where does this come from?
// always returns an array of roles
const retrieveRoles = async () => {
try {
// does this need to be deserialized?
let temp = await cache.get();
// do you want to throw an error if null?
return temp === null ? defaultRoles : temp;
} catch (error) {
console.log("initialization Error: ", error);
return defaultRoles;
}
};
// define the function
const initialize = async() => {
// wait for the roles
const roles = await retrieveRoles();
// then dispatch
dispatch({type: 'initialize', roles});
}
// execute the function
initialize();
}, [dispatch]); // run once on mount - dispatch should not change
// should use another useEffect to push changes
useEffect(() => {
cache.set(state.roles);
}, [state.roles]); // run whenever roles changes
// maybe this should be a context provider instead of a hook
// but this is just an example
return [state, dispatch];
};

Can anyone please tell me what's wrong this socket event?

I've emitted two events on user joined & left (user_joined and user_left). It's working on the server-side but not working on the client-side.
Server-side code: (it's working, showing console.log on every connection)
io.on('connection', function (socket) {
const id = socket.id;
/**
* User Join Function
*/
socket.on('join', function ({ name, room }) {
const { user } = addUser({id, name, room}); // add user to users array
socket.join(user.room);
socket.emit('user_joined', users); // emit event with modified users array
console.log(id, 'joined')
})
/**
* User Disconnect function
*/
socket.on('disconnect', () => {
removeUser(id); // remove user form users array
socket.emit('user_left', users); // emit event with modified users array
console.log(id, 'left')
})
})
Client-side code: (Not firing on user_joined or user_left)
const [players, setPlayers] = useState([]);
const ENDPOINT = 'localhost:5000';
socket = io(ENDPOINT);
useEffect(() => {
const name = faker.name.firstName() + ' ' + faker.name.lastName();
socket.emit('join', {name, room: 'global'}); // it's working fine
return () => {
socket.emit('disconnect');
socket.off();
}
}, [])
useEffect(() => {
socket.on('user_joined', (users) => {
setPlayers(users);
}); // >>> Not Working <<<
socket.on('user_left', (users) => {
setPlayers(users);
}); // >>> Not Working <<<
console.log(socket) // it's working fine
}, [players]);
The socket instance needs to be created only once. In your case, it is getting created on every re-render. Also you do not need 2 useEffects.
Put the creation of socket instance and merge your 2 useEffects into 1 and provide an empty array as dependency. With this, your useEffect is executed only once and not on every re-render.
Try this
const [players, setPlayers] = useState([]);
useEffect(() => {
const ENDPOINT = 'localhost:5000';
socket = io(ENDPOINT);
const name = faker.name.firstName() + ' ' + faker.name.lastName();
socket.emit('join', {name, room: 'global'});
socket.on('user_joined', (users) => {
setPlayers(users);
});
socket.on('user_left', (users) => {
setPlayers(users);
});
console.log(socket);
return () => {
socket.emit('disconnect');
socket.off();
}
}, []);
...
If you want to use the socket instance in other places of your component then make use of useRef. With useRef, you always get the same instance unless you mutate it.
create socket with refs
...
const [players, setPlayers] = useState([]);
const ENDPOINT = 'localhost:5000';
const socketInstance = useRef(io(ENDPOINT));// in react, with useRef, you always get the same instance unless you mutate it.
useEffect(() => {
// socketInstance.current = io(ENDPOINT);
const name = faker.name.firstName() + ' ' + faker.name.lastName();
socketInstance.current.emit('join', {name, room: 'global'});
socketInstance.current.on('user_joined', (users) => {
setPlayers(users);
});
socketInstance.current.on('user_left', (users) => {
setPlayers(users);
});
console.log(socketInstance.current);
return () => {
socketInstance.current.emit('disconnect');
socketInstance.current.off();
}
}, []);
...

Converting from class to functional component with async state setting

I have a simple class-based component that I'm trying to convert to a function-based component, but am running into all kinds of dead ends.
My component is a straightforward adaptation of the boilerplate gifted-chat package, and uses Watson Assistant as a backend to provide responses. There's nothing complex about the backend part, these are just thin wrappers on Watson Assistants's API:
getSessionID = async (): Promise<string>
gets a session ID for use in communicating with the backend, and
sendReply = async (reply: string, sessionID: string): Promise<string>
returns Assistant's response to the string provided as a reply. These are not the source of the trouble I'm having (the bodies of both could be replaced with return await "some string" and I'd have the same issues): the class-based version (below) works perfectly.
But I'm at a loss to figure out how to convert this to a functional form, in particular:
I'm struggling to find a suitable replacement for componentWillMount. Using useEffect with sessionID as state results in errors: getMessage gets called (even if I await) before the required sessionID is set.
I can avoid this by not making sessionID state (which it arguably shouldn't be) and just making it a global (as in the functional attempt below). But even if I do this:
After each user reply, and receipt of a response, the user reply is removed from the conversation, so that the entire conversation just consists of generated replies.
Both of these problems are, I think, linked to the lack of callbacks in the hook-based state setting idiom, but the issue could also lie elsewhere. In any case, I'm at a loss to know what to do.
Chatter.tsx (working class based version)
import React from 'react'
import { GiftedChat } from 'react-native-gifted-chat'
import WatsonAssistant from "../services/WatsonAssistant"
class Chatter extends React.Component {
state = {
messages: [],
sessionID: null,
}
componentWillMount() {
WatsonAssistant.getSessionID()
.then((sID) => {
this.setState( {
sessionID: sID,
} )
} )
.then(() => this.getMessage(''))
.catch((error) => {
console.error(error)
} )
}
onSend = (message = []): void => {
this.setState((previousState) => ( {
messages: GiftedChat.append(previousState.messages, message),
} ), () => {
this.getMessage(message[0].text.replace(/[\n\r]+/g, ' '))
} )
}
getMessage = async (text: string): Promise<void> => {
let response = await WatsonAssistant.sendReply(text, this.state.sessionID)
let message = {
_id: Math.round(Math.random() * 1000000).toString(),
text: response,
createdAt: new Date(),
user: {
_id: '2',
name: 'Watson Assistant',
},
}
this.setState((previousState) => ( {
messages: GiftedChat.append(previousState.messages, message),
} ))
}
render() {
return (
<GiftedChat
messages={ this.state.messages }
onSend={ messages => this.onSend(messages) }
user={ {
_id: 1,
} }
/>
)
}
}
export default Chatter
Chatter.tsx (failed function based attempt)
import React, {FC, ReactElement, useEffect, useState } from 'react'
import { GiftedChat } from 'react-native-gifted-chat'
import WatsonAssistant from "../services/WatsonAssistant"
let sessionID: string
const Chatter: FC = (): ReactElement => {
const [ messages, setMessages ] = useState([])
useEffect(() => {
const fetchData = async () => {
WatsonAssistant.getSessionID()
.then(sID => sessionID = sID )
.then(() => getMessage(''))
.catch((error) => {
console.error(error)
} )
}
fetchData()
}, [ ])
const onSend = async (message = []) => {
const newMessages = await GiftedChat.append(messages, message)
await setMessages(newMessages)
await getMessage(message[0].text.replace(/[\n\r]+/g, ' '))
}
const getMessage = async (text: string): Promise<void> => {
let response = await WatsonAssistant.sendReply(text, sessionID)
let message = {
_id: Math.round(Math.random() * 1000000).toString(),
text: response,
createdAt: new Date(),
user: {
_id: '2',
name: 'Watson Assistant',
},
}
await setMessages(await GiftedChat.append(messages, message))
}
return (
<GiftedChat
messages={ messages }
onSend={ messages => onSend(messages) }
user={ {
_id: 1,
} }
/>
)
}
export default Chatter
Chatter.tsx (working function based version)
import React, {FC, ReactElement, useEffect, useState } from 'react'
import { GiftedChat } from 'react-native-gifted-chat'
import WatsonAssistant from "../services/WatsonAssistant"
let sessionID: string
const Chatter: FC = (): ReactElement => {
const [ messages, setMessages ] = useState([])
useEffect(() => {
const fetchData = async () => {
WatsonAssistant.getSessionID()
.then(sID => sessionID = sID )
.then(() => getMessage('', []))
.catch((error) => {
console.error(error)
} )
}
fetchData()
}, [ ])
const onSend = async (message = []) => {
const newMessages = await GiftedChat.append(messages, message)
await setMessages(newMessages) // Apparently, no waiting goes on here
await getMessage(message[0].text.replace(/[\n\r]+/g, ' '), newMessages)
}
const getMessage = async (text: string, currentMessages): Promise<void> => {
let response = await WatsonAssistant.sendReply(text, sessionID)
let message = {
_id: Math.round(Math.random() * 1000000).toString(),
text: response,
createdAt: new Date(),
user: {
_id: '2',
name: 'Watson Assistant',
},
}
await setMessages(await GiftedChat.append(currentMessages, message))
}
return (
<GiftedChat
messages={ messages }
onSend={ messages => onSend(messages) }
user={ {
_id: 1,
} }
/>
)
}
export default Chatter
Ok, since I don't have your full code I'm not sure this will just work as-is (in particular without the types from your dependencies I'm not sure if/how much the compiler will complain), but should give you something you can adapt easily enough.
const reducer = ({ messages }, action) => {
switch (action.type) {
case 'add message':
return {
messages: GiftedChat.append(messages, action.message),
};
case 'add sent message':
return {
// Not sure if .append is variadic, may need to adapt
messages: GiftedChat.append(messages, action.message, action.message[0].text.replace(/[\n\r]+/g, ' ')),
}
}
};
const Chatter = () => {
const [sessionID, setSessionID] = useState(null);
const [messages, dispatch] = useReducer(reducer, []);
const getMessage = async (text: string, sessionID: number, type: string = 'add message'): Promise<void> => {
const response = await WatsonAssistant.sendReply(text, sessionID);
const message = {
_id: Math.round(Math.random() * 1000000).toString(),
text: response,
createdAt: new Date(),
user: {
_id: '2',
name: 'Watson Assistant',
},
};
dispatch({
type,
message,
});
};
useEffect(() => {
const fetchData = async () => {
WatsonAssistant.getSessionID()
.then(sID => (setSessionID(sID), sID))
.then(sID => getMessage('', sID))
.catch((error) => {
console.error(error)
});
}
fetchData();
}, []);
return (
<GiftedChat
messages={messages}
onSend={messages => getMessage(messages, sessionID, 'add sent message')}
user={{
_id: 1,
}}
/>
);
};
Main difference is useReducer. As far as I can tell in the original code you had two actions: append this message or append this message and then a copy of it with the text regex replaced. I've used different dispatches to the reducer to handle the cases rather than the callback to setState. I've modified your attempt at useEffect, here I'm (ab)using the comma operator to return the ID returned from the service so that it can be fed directly to getMessage as a parameter rather than relying on state that hasn't been updated yet.
I'm still kinda skeptical in general about the hooks API, but assuming this works I actually think it simplifies the code here.

Categories