React Native WebSocket onmessage Firing After Re-rendering - reactjs

I'm encountering a really weird issue with react native and websockets. I'm using a functional component, assigning the websocket to useRef, and there are a few other react hooks as well. When the view loads the websocket is loaded and works just fine. The messages come in just fine as well.
However when I call a hook unrelated to the websocket, the websocket's "onmessage" gets called.
const Home = ({ navigation }) => {
const store = useStore(); // from mobx store using React.useContext
const [riders, setRiders] = useState(store.schedule.riders);
const wss = useRef(null);
useEffect(() => {
const driverId = store.authentication.user.driver;
wss.current = new WebSocket(url);
wss.current.onopen = () => console.log('ws opened');
wss.current.onclose = (e) => console.log('ws closed', e.code, e.reason);
wss.current.onmessage = (e) => {
console.log('onmessage');
};
return () => {
wss.current.close();
};
}, []);
const handleStatusPress = () => {
// AFTER THIS EXECUTES "onmessage" GETS CALLED
store.schedule.updateStatus(ride.id, 'completed');
};
return (
<View>
{riders.map((s) => {
return (
<Animated.View
key={s.id}
style={{ ...styles.riderCard }}
>
<View style={styles.column}>
<Text style={styles.riderName}>{s.name}</Text>
<Text style={styles.riderType}>PICKUP</Text>
<Text style={styles.riderTime}>{moment(parseInt(s.datetime, 10)).format('h:mm A')}</Text>
</View>
<TouchableOpacity
style={styles.bubble}
onPress={() => {
handleStatusPress(s);
}}
>
<Text style={styles.riderStatus}>{s.status}</Text>
</TouchableOpacity>
</Animated.View>
);
})}
</View>
);
});

Related

Getting "Can't perform a React state update on an unmounted component" only the first time

I am creating a ToDo app. This app has two screens: Todos and Done. I'm using BottomTabNavigator to switch between these screens. These two screens has list of todos. The todos component shows the undone todos and the Done component shows the done todos. There's a checkbox on the left and Trash icon on the right of every single todo. When a todo from Todos page is checked then it moves to the Done page. The issue is: after switching to the Done screen from Todos for the first time then after unchecking the todo there gives this warning:
Warning: Can't perform a React state update on an unmounted component. This is a no-op, but it indicates a memory leak in your application. To fix, cancel all subscriptions and asynchronous tasks in a useEffect cleanup function.
in SingleTodo (at Done.tsx:74)
After this, the app is running perfectly. As I'm not sure which component is causing this error that'w why I'm sharing the minimal version of the code.
I have set up Bottom Tab navigator component like this:
import stuff..
...
const HomeTab = () => {
return (
<Tab.Navigator
screenOptions={({route}) => ({
headerShown: false,
tabBarIcon: ({focused, color, size}) => {
let iconName = '';
size = focused ? 25 : 20;
if (route.name === 'To-Do') {
iconName = 'clipboard-list';
} else if (route.name === 'Done') {
iconName = 'clipboard-check';
}
return <FontAwesome5Icon name={iconName} size={size} color={color} />;
},
tabBarActiveTintColor: '#0080ff',
tabBarInactiveTintColor: '#777777',
tabBarLabelStyle: {fontSize: 15, fontWeight: 'bold'},
})}>
<Tab.Screen name="To-Do" component={Todos} />
<Tab.Screen name="Done" component={Done} />
</Tab.Navigator>
);
};
export default HomeTab;
As you can see, there are 2 components here. One is Todos. The code for this component is as follows:
import stuff...
...
const Todos = ({navigation}) => {
const dispatch = useAppDispatch();
const {todos}: {todos: TodoInterface[]} = useAppSelector(
state => state.todoReducer,
);
useEffect(() => {
loadTodos();
}, []);
const loadTodos = () => {
AsyncStorage.getItem('todos').then(todos => {
const parsedTodos: TodoInterface[] = JSON.parse(todos || '{}');
dispatch(setAllTodo(parsedTodos));
});
};
return (
<HideKeyboard>
<View style={styles.body}>
<FlatList
data={todos.filter(todo => todo.done !== true)}
renderItem={({item, index}) => {
const firstChild = index == 0 ? {marginTop: 5} : {};
return (
<TouchableOpacity
style={[styles.todoWrp, firstChild]}
onPress={() => todoPressHandler(item.todoId)}>
<SingleTodo // ***The code for this one is given below***
title={item.title}
subtitle={item.subTitle}
done={item?.done}
todoId={item.todoId}
/>
</TouchableOpacity>
);
}}
keyExtractor={(item, i) => item.todoId}
/>
<TouchableOpacity style={styles.addBtn} onPress={addBtnHandler}>
<FontAwesome5 name="plus" color="#fff" size={25} />
</TouchableOpacity>
</View>
</HideKeyboard>
);
}
The code for SingleTodo is as follows:
const SingleTodo = ({title, subtitle, done: doneProp, todoId}: Props) => {
const [done, setDone] = useState(doneProp);
const dispatch = useAppDispatch();
const {todos}: TodosType = useAppSelector(state => state.todoReducer);
const checkBoxHandler = (val: boolean) => {
const todoList: TodoInterface[] = [...todos];
const index = todos.findIndex(todo => todo.todoId === todoId);
todoList[index].done = val;
AsyncStorage.setItem('todos', JSON.stringify(todoList)).then(() => {
dispatch(setAllTodo(todoList));
setDone(val);
});
};
const deleteHandler = () => {
const todoList: TodoInterface[] = [...todos];
const index = todos.findIndex(todo => todo.todoId === todoId);
todoList.splice(index, 1);
AsyncStorage.setItem('todos', JSON.stringify(todoList)).then(() => {
dispatch(setAllTodo(todoList));
});
};
return (
<View style={styles.body}>
<CheckBox
value={done}
onValueChange={val => checkBoxHandler(val)}
style={styles.checkbox}
/>
<View>
<Text style={[styles.title, GlobalStyle.IndieFont]}>{title}</Text>
<Text style={[styles.subtitle, GlobalStyle.IndieFont]}>{subtitle}</Text>
</View>
<View style={styles.trashWrp}>
<TouchableOpacity onPress={deleteHandler}>
<FontAwesome5Icon
style={styles.trashIcon}
name="trash"
color="#e74c3c"
size={20}
/>
</TouchableOpacity>
</View>
</View>
);
};
export default SingleTodo;
The code for Done component is similar to Todos component. The only changes is on the data property of the component
<FlatList
data={todos.filter(todo => todo.done === true)}
...
other props...
...
/>
It's happening every time you use this, it is just shown once to not spam the console.
const checkBoxHandler = (val: boolean) => {
const todoList: TodoInterface[] = [...todos];
const index = todos.findIndex(todo => todo.todoId === todoId);
todoList[index].done = val;
AsyncStorage.setItem('todos', JSON.stringify(todoList)).then(() => {
dispatch(setAllTodo(todoList));
setDone(val);
});
};
const deleteHandler = () => {
const todoList: TodoInterface[] = [...todos];
const index = todos.findIndex(todo => todo.todoId === todoId);
todoList.splice(index, 1);
AsyncStorage.setItem('todos', JSON.stringify(todoList)).then(() => {
dispatch(setAllTodo(todoList));
});
};
Basically, you call the function, and the todo is unmounted from the state, but the function is not completed yet and you get that warning.
The solution is to lift everything related to the deleteHandler and checkBoxHandler from your children (Todo) to your parent (Todos), and pass it to Todo as props. Since parent is always mounted, deleting the todo will not unmount the parent and therefore, delete function will not be interrupted.

react_native + iframe + firebase : How could I serve the videoId of next index from realtime database?

now i use react native, expo project.
and i apply realtime database, iframe, FlatList.
and i use videoID instead of the 'scr= URL'.
i hope to autoplay by auto changing the videoId in my realtime database.
i think it can be possible by using index number.
and when i console.log selected index, it presented on terminal.
but i don't know how to apply that path for my code.
this is my realtime database.
enter image description here
and this is my codes.
import YoutubePlayer from "react-native-youtube-iframe";
const [state, setState] = useState([])
const [cardID, setCardID] = useState(["i4S5hvPG9ZY"])
const [playing, setPlaying] = useState(true);
const onStateChange = useCallback((state) => {
if (state === "ended") {
setPlaying(true)
}
}, []);
const onPress = ({ item, index }) => {
console.log({ index })
return (
setCardID(item.id.videoId)
)
}
...
useEffect(() => {
setLoading(true);
firebase_db.ref('/TGBSitems')
.once('value')
.then((snapshot) => {
console.log("TGBS에서 데이터 가져왔습니다!!")
let TGBSitems = snapshot.val()
setState(TGBSitems)
setTotalDataSource(TGBSitems);
setLoading(false);
})
.catch(err => { setLoading(false); setError(err); })
}, []);
...
return (
...
<View>
<YoutubePlayer
height={200}
play={playing}
videoId={cardID}
onChangeState={onStateChange}
// playList
/>
</View>
...
<FlatList
data={state}
// ItemSeparatorComponent={ItemSeparatorView}
keyExtractor={(index) => index.toString()}
renderItem={({ item, index }) => (
<View style={styles.cardContainer}>
<TouchableOpacity style={styles.card} onPress={() => onPress({ item, index })}>
<Image style={styles.cardImage} source={{ uri: item.snippet.thumbnails.medium.url }} />
<View style={styles.cardText}>
<Text style={styles.cardTitle} numberOfLines={1}>{item.snippet.title}</Text>
<Text style={styles.cardDesc} numberOfLines={3}>{item.snippet.description}</Text>
<Text style={styles.cardDate}>{item.snippet.publishedAt}</Text>
<Text style={styles.cardDate}>{item.id.videoId}</Text>
<Text style={styles.cardDate}>{index}</Text>
</View>
...

State does not update its value using AsyncStorage in react-native

I am trying to save a value to async storage and then navigate to the right page depending on what the value outcome is from the Async storage. I can store data in AsyncStorage but my states does not update, I have to reload the app in order for the state to update. here is my code:
Here I have a Welcome/Obnoarding screen. I want this screen to only show to the new app users. So when a user presses the continue button I want to save a value to the Async storage so that the next time they log in they don't have to see the onboarding page again. Here is my Onboarding page:
const WelcomeScreen: FC<IWelcomeScreen> = ({ navigation }) => {
const { width, height } = Dimensions.get("window");
const btnText = "Contiunue";
const title = "Book";
const subTitle = "Fab";
let [fontsLoaded] = useFonts({
PinyonScript_400Regular,
});
const continueBtn = async () => {
try {
await AsyncStorage.setItem('#viewedOnboarding', 'true');
} catch (error) {
console.log('Error #setItem: ', error);
};
};
if (!fontsLoaded) {
return <Text>...Loading</Text>;
} else {
return (
<View style={containerStyle(height, width).container}>
<ImageBackground
resizeMode={"cover"}
style={styles.image}
source={require("../assets/model.jpg")}
>
<LinearGradient
colors={["#00000000", "#000000"]}
style={styles.gradient}
>
<View style={styles.container}>
<View style={styles.logoTextContainer}>
<Text style={styles.logoText}>{title}</Text>
<Text style={styles.logoText}>{subTitle}</Text>
</View>
<ContinueBtn label={btnText} callback={continueBtn} />
</View>
</LinearGradient>
</ImageBackground>
</View>
);
}
};
In my AppNavigator I want to decide which navigation the user should see. But when I press the continue page my app does not navigate to my TabsNavigator. It stays on my Onboarding page but if I refresh the app then the app navigates to my Tabs navigator. here is the code where I determine where the user should be depending if they are a new user or a "old" user:
const WelcomeScreen: FC<IWelcomeScreen> = ({ navigation }) => {
const { width, height } = Dimensions.get("window");
const btnText = "Contiunue";
const title = "Book";
const subTitle = "Fab";
let [fontsLoaded] = useFonts({
PinyonScript_400Regular,
});
const continueBtn = async () => {
try {
await AsyncStorage.setItem('#viewedOnboarding', 'true');
} catch (error) {
console.log('Error #setItem: ', error);
};
};
if (!fontsLoaded) {
return <Text>...Loading</Text>;
} else {
return (
<View style={containerStyle(height, width).container}>
<ImageBackground
resizeMode={"cover"}
style={styles.image}
source={require("../assets/model.jpg")}
>
<LinearGradient
colors={["#00000000", "#000000"]}
style={styles.gradient}
>
<View style={styles.container}>
<View style={styles.logoTextContainer}>
<Text style={styles.logoText}>{title}</Text>
<Text style={styles.logoText}>{subTitle}</Text>
</View>
<ContinueBtn label={btnText} callback={continueBtn} />
</View>
</LinearGradient>
</ImageBackground>
</View>
);
}
};
Setting a value in the async storage will not trigger a rerender of your AppNavigator. Thus, if the user presses the continue button, then nothing will happen visually, since the state of AppNavigator has not changed. If you refresh the app, the flag, which you have set previously using the setItem function, will be reloaded in AppNavigator on initial rendering. This is the reason why it works after refreshing the application.
For this kind of problem, I would suggest that you use a Context for triggering a state change in AppNavigator.
Here is a minimal example on how this would work. I have added comments in the code to guide you.
For the sake of simplicity, we will make the following assumption:
We have two screens in a Stack, one is the WelcomeScreen, the other one is called HomeScreen.
Notice that we use conditional rendering for the screens depending on our application context. You can add whatever screens you want, even whole navigators (this would be necessary if your navigators are nested, but the pattern stays the same).
App
export const AppContext = React.createContext()
const App = () => {
// it is important that the initial state is undefined, since
// we need to wait for the async storage to return its value
// before rendering anything
const [hasViewedOnboarding, setHasViewedOnboarding] = React.useState()
const appContextValue = useMemo(
() => ({
hasViewedOnboarding,
setHasViewedOnboarding,
}),
[hasViewedOnboarding]
)
// retrieve the onboarding flag from the async storage in a useEffect
React.useEffect(() => {
const init = async () => {
const value = await AsyncStorage.getItem('#viewedOnboarding')
setHasViewedOnboarding(value != null ? JSON.parse(value) : false)
}
init()
}, [])
// as long as the flag has not been loaded, return null
if (hasViewedOnboarding === undefined) {
return null
}
// wrap everything in AppContext.Provider an pass the context as a value
return (
<AppContext.Provider value={appContextValue}>
<NavigationContainer>
<Stack.Navigator>
{!hasViewedOnboarding ? (
<Stack.Screen name="Welcome" component={WelcomeScreen} />
) : (
<Stack.Screen
name="Home"
component={HomeScreen}
/>
)}}
</Stack.Navigator>
</NavigationContainer>
</AppContext.Provider>
)
}
Now, in your WelcomeScreen you need to access the context and set the state after the async value has been stored.
const WelcomeScreen: FC<IWelcomeScreen> = ({ navigation }) => {
// access the context
const { setHasViewedOnboarding } = useContext(AppContext)
const { width, height } = Dimensions.get("window");
const btnText = "Contiunue";
const title = "Book";
const subTitle = "Fab";
let [fontsLoaded] = useFonts({
PinyonScript_400Regular,
});
const continueBtn = async () => {
try {
await AsyncStorage.setItem('#viewedOnboarding', 'true');
setHasViewedOnboarding(true)
} catch (error) {
console.log('Error #setItem: ', error);
};
};
if (!fontsLoaded) {
return <Text>...Loading</Text>;
} else {
return (
<View style={containerStyle(height, width).container}>
<ImageBackground
resizeMode={"cover"}
style={styles.image}
source={require("../assets/model.jpg")}
>
<LinearGradient
colors={["#00000000", "#000000"]}
style={styles.gradient}
>
<View style={styles.container}>
<View style={styles.logoTextContainer}>
<Text style={styles.logoText}>{title}</Text>
<Text style={styles.logoText}>{subTitle}</Text>
</View>
<ContinueBtn label={btnText} callback={continueBtn} />
</View>
</LinearGradient>
</ImageBackground>
</View>
);
}
};

Objects are not valid as a React child, use an array instead

I am trying to render the first and last name from a json request using axios.
I am getting the following error you see in the title. I have included a snack example here reproducing the error exactly as well as added the code below.
Thank you
const plsWork = () => {
// Make a request for a user with a given ID
return axios.get('https://randomuser.me/api')
.then(({data}) => {
console.log(data);
return data
})
.catch(err => {
console.error(err);
});
}
const userName = (userInfo) => {
const {name: {first, last}} = userInfo;
return {first}, {last};
}
export default function App() {
const [data, setData] = React.useState(' ')
const [userInfos, setUserInfos] = React.useState([]);
React.useEffect(() => {
plsWork().then(randomData => {
setData(JSON.stringify(randomData, null, 4) || 'No user data found.')
setUserInfos(randomData.results)
})
}, []);
return (
<View>
<ScrollView>
{
userInfos.map((userInfo, idx) => (
<Text key={idx}>
{userName(userInfo)}
</Text>
))
}
<Text style={{color: 'black', fontSize: 15}}>
{data}
</Text>
</ScrollView>
</View>
);
}
You have to return a React Component in the userName function.
In the line 21:
Change from return {first}, {last} to return <>{first}, {last}</>.
It should work!
Here is code edited: snack expo

React Native Deck Swiper

I am trying to make a GET request to an enpoint using the two functional components below to display in my react native deck swipper
//using fetch
const getDataUsingFetch = () => {
fetch(latestNews+ApiKey)
.then((response) => response.json())
.then((responseJson) => {
// set the state of the output here
console.log(responseJson);
setLatestNews(responseJson);
})
.catch((error) => {
console.error(error);
});
}
//using anxios
//asynchronous get request call to fetech latest news
const getDataUsingAnxios = async () => {
//show loading
setLoading(true);
setTimeout(async () => {
//hide loading after the data has been fetched
setLoading(false);
try {
const response = await axios.get(latestNews+ApiKey);
setLatestNews(response.data);
setLoading(false);
console.log(getLatestNews);
} catch (error) {
// handle error
alert(error.message);
}
}, 5000);
};
Data returned when logged from console:
Array [
Object {
"category_id": "8",
"content": "Hi",
"created_at": "2020-11-12T12:43:03.000000Z",
"featured_image": "splash-background_1605184983.jpg",
"id": 19,
"news_url": "doerlife.com",
"title": "I m good how about you",
"updated_at": "2020-11-12T12:43:03.000000Z",
}....]
I now save the data into a state array
const [getLatestNews, setLatestNews] = useState([]);
Here is my swipper(some code ommited - not necessary)
<Swiper
ref={useSwiper}
//cards={categoryID(docs, "2")}
cards={getLatestNews}
cardIndex={0}
backgroundColor="transparent"
stackSize={2}
showSecondCard
cardHorizontalMargin={0}
animateCardOpacity
disableBottomSwipe
renderCard={(card) => <Card card={card} />}
.....
When I try to access any data in the array from my Card reusable component, e.g card.featured_image
I WILL GET THIS ERROR - TypeError: undefined is not an object (evaluating 'card.featured_image').
PLEASE CAN SOMEONE HELP ME.
//Card reusable component for deck swipper
import React from 'react'
import { View, Text, Image, ImageSourcePropType } from 'react-native'
import styles from './Card.styles'
const Card = ({ card }) => (
<View activeOpacity={1} style={styles.card}>
<Image
style={styles.image}
source={card.featured_image}
resizeMode="cover"
/>
<View style={styles.photoDescriptionContainer}>
<Text style={styles.title}>{`${card.title}`}</Text>
<Text style={styles.content}>{`${card.content}`}</Text>
<Text style={styles.details}>
Swipe Left to read news in details
</Text>
</View>
</View>
);
export default Card
I've done something similar to this before so I think I can help a bit. The problem here is that your getLatestNews state has not been updated yet before the cards render. You can fix the problem by having another state called "isDataReturned". Then, have a useEffect that triggers whenever getLatestNews's length changes. If getLatestNews's length is > 0, then you can set isDataReturned to be true and render the deck only when isDataReturned is true.
Here's a code sample that I made:
const [getLatestNews, setLatestNews] = useState([]);
const [dataIsReturned, setDataIsReturned] = useState(false)
useEffect(() => {
const fetchData = async () => {
const result = await axios(
'https://cat-fact.herokuapp.com/facts',
);
setLatestNews(result.data);
};
fetchData();
}, []);
useEffect(() => {
if (getLatestNews.length > 0) {
setDataIsReturned(true)
} else {
setDataIsReturned(false)
}
}, [getLatestNews.length])
if( dataIsReturned === true) {
return (
<View style={styles.container}>
<Swiper
cards={getLatestNews}
renderCard={(card) => {
return (
<View style={styles.card}>
<Text>{card.text}</Text>
</View>
)
}}
onSwiped={(cardIndex) => {console.log(cardIndex)}}
onSwipedAll={() => {console.log('onSwipedAll')}}
cardIndex={0}
backgroundColor={'#4FD0E9'}
stackSize= {3}>
</Swiper>
</View>)
} else {
return(<Text>Loading</Text>)
}
In the renderCard attribute, i changed it from
renderCard={(card) => <Cardz card={card} />}
to
renderCard={(card) => (card && <Cardz card={card} />) || null}
and it worked.

Resources