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

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>
);
}
};

Related

Trying to add multiple images in an object

I have a form object that holds all my input values. I am working with the react-native camera component and what I am trying to accomplish is every time a picture is added, it is added to the object. With my current code, I am getting invalid attempts to spread non-iterable instances.
import { TicketContext } from "../store/TicketContext";
function Pictures() {
const [hasCameraPermissions, setHasCameraPermissions] = useState();
const [picture, setPicture] = useState();
const { form, setForm } = useContext(TicketContext);
//Add picture to form //
const handleAddPicture = () => {
setForm([...(form ?? []), { picture: picture.uri }]);
setPicture();
};
const cameraRef = useRef();
//Get Permission to use Camera//
const handleCameraPermissions = async () => {
const cameraPermissions = await Camera.requestCameraPermissionsAsync();
setHasCameraPermissions(cameraPermissions.status === "granted");
};
// Check for permissions on load //
useEffect(() => {
handleCameraPermissions();
}, []);
if (hasCameraPermissions === undefined) {
return <Text>Permissions Required...</Text>;
} else if (!hasCameraPermissions) {
return <Text>Camera Permission Denied. Please change in settings.</Text>;
}
//Take Picture //
const handleTakePicture = async () => {
const options = { base64: true, exif: false, quality: 1 };
const newPicture = await cameraRef.current.takePictureAsync(options);
setPicture(newPicture);
};
if (picture) {
return (
<SafeAreaView style={styles.container}>
<Image
style={styles.preview}
source={{ uri: "data:image/jpg;base64," + picture.base64 }}
/>
<View style={styles.buttonContainer}>
<Button title="Add Picture" onPress={handleAddPicture} />
</View>
</SafeAreaView>
);
}
return (
<SafeAreaView style={styles.container}>
<Camera style={styles.cameraContainer} ref={cameraRef}>
<View style={styles.buttonContainer}>
<Button title="Take Picture" onPress={handleTakePicture} />
</View>
</Camera>
</SafeAreaView>
);
}
export default Pictures;
setPicture returns the state back to undefined
my state in context
const [form, setForm] = useState({});
//[form ,setForm]
// make sure you object keys are camel case.
const handleAddPicture = () => {
setForm([...(form ?? []), { picture: picture.uri }]);
setPicture();
};

React native tab view render after Api call

i'm using the tab view react native library to build a user account screen. my request is simple, how can i update the tab view content after an api call that fetches the user data?
function UserStoreScreen({ navigation, route }) {
const layout = useWindowDimensions();
const [index, setIndex] = React.useState(0);
const [userStore, setUserStore] = React.useState({});
const [routes] = React.useState([
{ key: "first", title: "Dressing" },
{ key: "second", title: "À propos" },
]);
const user = route.params;
// renders undefined
const FirstRoute = () => (
<>
<View style={styles.userContainer}>
<ListItem
image={`${IMAGES_BASE_URL}${userStore.photo}`}
title={`${userStore.username}`}
subTitle={`${userStore.store.length} Articles`}
/>
</View>
</>
);
const SecondRoute = () => (
<>
<View style={{ flex: 1, backgroundColor: "#ff4081" }} />
</>
);
const renderScene = SceneMap({
first: FirstRoute,
second: SecondRoute,
});
const getUser = async () => {
await axiosApi
.post("/getUserProducts", { user_id: user.user_id })
.then((response) => {
// didn't work since set state is async
setUserStore(response.data);
})
.catch((err) => {
console.log(err);
});
};
// Get store products
useEffect(() => {
getUser();
}, []);
return (
<Screen style={styles.screen}>
<TabView
navigationState={{ index, routes }}
renderScene={renderScene}
onIndexChange={setIndex}
initialLayout={{ width: layout.width }}
/>
</Screen>
);
}
is there a way to make the content of the tab view updated after i receive the data from the api call?
Yes, there is a way to forcefully re-mount a component. To do that, we can use key props like this:
return (
<Screen style={styles.screen}>
<TabView
key={JSON.stringify(userStore)}
navigationState={{ index, routes }}
renderScene={renderScene}
onIndexChange={setIndex}
initialLayout={{ width: layout.width }}
/>
</Screen>
);
How does key props work? Every time a component is re-rendering, it will check whether the key value is the same or not. If it's not the same, then force a component to re-render.
In this case we will always check if userStore value has changed or not.

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.

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 WebSocket onmessage Firing After Re-rendering

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>
);
});

Resources