How to update state on App start in react native - react-native

I am trying to read from the AsyncStorage and update my store ( using easy-peasy ) on app start.
My thought process was to call fetch the data from AsyncStorage using useEffect with the second argument of an empty array to only fetch once on app start and update the store with that value using an action.
But that doesn't work i get the error invalid hook call. Any insights on how to solve this or what the correct approach would be are appreciated !
App.ts
export default function App() {
useEffect(() => {
readData();
}, []);
return (
<StoreProvider store={store}>
<SafeAreaProvider>
<Navigation />
<StatusBar />
</SafeAreaProvider>
</StoreProvider>
);
}
// This needs to be called when app is started
const readData = async () => {
try {
const secret = await storage.getItem('secret');
const initializeState = useStoreActions(
(actions) => actions.initializeState
);
initializeState({
payload: {
secret,
},
});
console.log("executed action")
} catch (e) {
console.log('Failed to fetch the input from storage', e);
}
};
STORE
initializeState: action((state, payload) => {
state.secret = payload.secret
}),
ERROR
Failed to fetch the input from storage [Error: Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen for one of the following reasons:
1. You might have mismatching versions of React and the renderer (such as React DOM)
2. You might be breaking the Rules of Hooks
3. You might have more than one copy of React in the same app
See https://reactjs.org/link/invalid-hook-call for tips about how to debug and fix this problem.]

export default function App() {
useEffect(() => {
setTimeout(()=>{ // add setTimeout() may be this work for you
readData();
},500)
}, []);
return (
<StoreProvider store={store}>
<SafeAreaProvider>
<Navigation />
<StatusBar />
</SafeAreaProvider>
</StoreProvider>
);
}
// This needs to be called when app is started
const readData = async () => {
try {
const secret = await storage.getItem('secret');
const initializeState = useStoreActions(
(actions) => actions.initializeState
);
initializeState({
payload: {
secret,
},
});
console.log("executed action")
} catch (e) {
console.log('Failed to fetch the input from storage', e);
}
};

You need to move you readData function into the App component since you're using a hook (useStorageActions) inside that function and you can only call hooks at the top level. You should take a look at the rules of react hooks.

Related

React Native component not re-rendering when state is updated

I have a component in my React Native app that displays a list of pending friends. This component makes a GET request to an API to retrieve the list of pending friends and then uses a useEffect hook to map over the list and render each friend as a Pressable component. I'm also using the useFocusEffect hook to make the get request when the screen renders.
Here is the relevant code for the component:
const Pending = () => {
const [pendingFriends, setPendingFriends] = useState(null)
let pendingFriendsRender = []
useEffect(() => {
if (pendingFriends !== null) {
for(let i = 0; i < pendingFriends.length; i++) {
pendingFriendsRender.push(
<Pressable key={i} style={styles.friend}>
<Text style={styles.friendText}>{pendingFriends[i].username}</Text>
</Pressable>
)
}
}
}, [pendingFriends])
useFocusEffect(
useCallback(() => {
async function fetchData() {
const accessToken = await AsyncStorage.getItem('accessToken')
try {
const res = await instance.get('/pending_friends', {
headers: { authorization: `Bearer ${accessToken}`},
})
setPendingFriends(res.data)
} catch (error) {
console.log(error.response.status)
}
}
fetchData()
}, [])
)
return(
<View style={styles.friendsContainer}>
{pendingFriendsRender}
</View>
)
}
I have tried using an empty array as the second argument in the useEffect hook but that approach has not worked. I also tried removing the useEffect hook so the if statement with the for loop stands at the top of the component without the hook, that worked but I can't update it in this way after the component rendered. I checked the API and it is returning the correct data.
The first useEffect you have really isn't needed. You can map through your state inside of your JSX. Anytime the state changes, the component will be re-rendered:
// Need a default here, could also set some loading state when fetching your data
if(pendingFriends === null) {
return <>Loading...</>
}
return(
<View style={styles.friendsContainer}>
{pendingFriends.map((friend, i) => {
return (
<Pressable key={friend.id} style={styles.friend}>
<Text style={styles.friendText}>{friend.username}</Text>
</Pressable>
)
})}
</View>
)
Also keep in mind, it's not recommended to use the index as the key, it can lead to unexpected bugs and issues. Instead use a unique string key (as shown above).
React: using index as key for items in the list
pendingFriendsRender should be the state:
const [pendingFriendsRender, setPendingFriendsRender] = useState([])
Instead of
let pendingFriendsRender = []
Then just clone the array so you lose reference to the object and add the new element
const newPendingFriendsRender = [...pendingFriendsRender, newElement]
or you can use FlatList to make it easier.

Could not find "store" in the context of "Connect(HomeScreen)". Either wrap the root component in a... or pass a custom React context provider

Check multitude of questioned already asked and but still can't figure this one out.
We are rewriting our authentication layer using
export default AuthContext = React.createContext();
and wrapping it around our AppNavigator
function AppNavigator(props) {
const [state, dispatch] = useReducer(accountReducer, INITIAL_STATE);
const authContext = React.useMemo(
() => ({
loadUser: async () => {
const token = await keychainStorage.getItem("token");
if (token) {
await dispatch({ type: SIGN_IN_SUCCESS, token: token });
}
},
signIn: async (data) => {
client
.post(LOGIN_CUSTOMER_RESOURCE, data)
.then((res) => {
const token = res.data.accessToken;
keychainStorage.setItem("token", token);
dispatch({ type: SIGN_IN_SUCCESS, token: token });
})
.catch((x) => {
dispatch({ type: SIGN_IN_FAIL });
});
},
signOut: () => {
client.delete({
LOGOUT_CUSTOMER_RESOURCE
});
dispatch({ type: SIGN_OUT_SUCCESS });
}
}),
[]
);
console.log("token start", state.token);
return (
<AuthContext.Provider value={authContext}>
<NavigationContainer
theme={MyTheme}
ref={(navigatorRef) => {
NavigationService.setTopLevelNavigator(navigatorRef);
}}
onStateChange={(state) => {
NavigationService.setAnalytics(state);
}}
>
<AppStack.Navigator initialRouteName="App" screenOptions={hideHeader}>
{state.token != null ? (
<AppStack.Screen name="App" component={AuthMainTabNavigator} />
) : (
<>
<AppStack.Screen name="App" component={MainTabNavigator} />
<AppStack.Screen name="Auth" component={AuthNavigator} />
</>
)}
</AppStack.Navigator>
</NavigationContainer>
</AuthContext.Provider>
);
}
export default AppNavigator;
App.js - render fucnction
<Root>
<StoreProvider store={store} context={AuthContext}>
<PersistGate loading={null} persistor={persistor}>
<SafeAreaProvider>
<AppNavigator context={AuthContext}/>
</SafeAreaProvider>
</PersistGate>
</StoreProvider>
</Root>
HomeScreen.js
export default connect(mapStateToProps, mapDispatchToProps, null, { context: AuthContext })(HomeScreen);
But still receiving
Error: Could not find "store" in the context of "Connect(HomeScreen)". Either wrap the root component in a <Provider>, or pass a custom React context provider to <Provider> and the corresponding React context consumer to Connect(HomeScreen) in connect options.
We have gone through the REDUX documentation:
https://react-redux.js.org/using-react-redux/accessing-store#using-the-usestore-hook
Simply can not work out why we are receiving this error.
I'm not sure what you're trying to accomplish here, but this is very wrong:
export default connect(mapStateToProps, mapDispatchToProps, null, { context: AuthContext })(HomeScreen);
It looks like you're mixing up two different things. You're trying to create a context for use with your own auth state, but you're also trying to use that same context instance to override React-Redux's own default context instance. Don't do that! You should not be passing a custom context instance to connect and <Provider> except in very rare situations.
I understand what you are trying to achieve only after reading through your discussion in the comments with #markerikson.
The example from the React Navigation docs creates a context AuthContext in order to make the auth functions available to its descendants. It needs to do this because the state and the dispatch come from the React.useReducer hook so they only exist within the scope of the component.
Your setup is different because you are using Redux. Your state and dispatch are already available to your component through the React-Redux context Provider and can be accessed with connect, useSelector, and useDispatch. You do not need an additional context to store your auth info.
You can work with the context that you already have using custom hooks. Instead of using const { signIn } = React.useContext(AuthContext) like in the example, you can create a setup where you would use const { signIn } = useAuth(). Your useAuth hook can access your Redux store by using the React-Redux hooks internally.
Here's what that code looks like as a hook:
import * as React from 'react';
import * as SecureStore from 'expo-secure-store';
import { useDispatch } from "react-redux";
export const useAuth = () => {
// access dispatch from react-redux
const dispatch = useDispatch();
React.useEffect(() => {
// same as in example
}, []);
// this is the same as the example too
const authContext = useMemo(
() => ({
signIn: async data => {
dispatch({ type: 'SIGN_IN', token: 'dummy-auth-token' });
},
signOut: () => dispatch({ type: 'SIGN_OUT' }),
signUp: async data => {
dispatch({ type: 'SIGN_IN', token: 'dummy-auth-token' });
},
}),
[]
);
// but instead of passing that `authContext` object to a `Provider`, just return it!
return authContext;
}
In your component, which must be inside your React-Redux <Provider>:
function App() {
const { signIn } = useAuth();
const [username, setUsername] = React.useState('');
return (
<Button onPress={() => signIn(username)}>Sign In</Button>
)
}

Queue navigation until screen is mounted and then navigate

I am trying to navigate to a certain screen on my bottom-tab-navigator when a user opens the app by clicking a notification.
Looking into the official docs Navigating without the navigation prop, my setup of my main navigator is as follows:
import {navigationRef, isReadyRef} from './root';
const MainNav = _ => {
if (isLoading) {
return isFirstTime ? (<OnBoarding />) : (<SplashScreen />);
}
return (
<NavigationContainer
ref={navigationRef}
onReady={() => {isReadyRef.current = true}}>
{!token ? <AuthNav /> : <AppNav />}
</NavigationContainer>
);
}
My root.js is as follows:
import * as React from 'react';
export const isReadyRef = React.createRef();
export const navigationRef = React.createRef();
export function navigate(name, params) {
if (isReadyRef.current && navigationRef.current) {
// Perform navigation if the app has mounted
navigationRef.current.navigate(name, params);
} else {
// You can decide what to do if the app hasn't mounted
// You can ignore this, or add these actions to a queue you can call later
console.log('Not mounted yet.')
}
}
And I had added the OneSignal event listener in my root index.js as following:
const App = _ => {
useEffect(() => {
OneSignal.addEventListener('opened', onOpened);
return () => OneSignal.removeEventListener('opened', onOpened);
}, []);
return {
<StoreProvider store={store}>
<MainNav />
</StoreProvider>
}
}
And my onOpened function is as follows:
import {navigate} from '../nav/root';
const onOpened = ({notification}) => {
if(notification.type == 'New Request'){
navigate('Notifications');
}
}
But when I test it as expected Not mounted yet. is printed to console. So I want to
add these actions to a queue you can call later
as stated by the official react navigation docs but I am not sure how to do this. I found react-native-queue but it is no longer being maintained and using a setTimeout just seems like an ugly hack cause the load time varies. So is there a better approach or solution that I can use to navigate only after the loading is done (I am thinking of using redux for this) and my navigators have been mounted (not sure how to do this)?

React Native: TypeError: this.state.schedule.map is not an object

Hey I am new to React Native and currently I'm trying to put data in a picker using data from API. I'm confused that it got error say TypeError: null is not an object (evaluating this.state.schedules.map). Is there something wrong with the state or is there any concept that I misunderstood
Here is fetch API
export function getSchedule (token, resultCB) {
var endpoint = "/api/getList"
let header = {
"Content-Type": "application/x-www-form-urlencoded",
"Authorization": "Bearer " + token
};
return dispatch => {
return fetchAPI(endpoint, 'GET', header)
.then((json) => {
dispatch({ type: t.SCHEDULE, schedules: json.datas.description });
resultCB(json.schedules)
})
.catch((error) => {
dispatch({ type: types.EMPTY_SCHEDULE });
resultCB(error)
})
}
}
this is where i put my picker
export const mapStateToProps = state => ({
token: state.authReducer.token,
message: state.authReducer.message,
schedules: state.authReducer.schedules
});
export const mapDispatchToProps = (dispatch) => ({
actionsAuth: bindActionCreators(authAction, dispatch)
});
class Change extends Component {
constructor(){
super();
this.state={
staffId: "",
schedule: '',
type_absen: 1,
schedules: null
}
}
componentDidMount(){
this.props.actionsAuth.getSchedule(this.props.token);
}
render() {
return (
<View style={styles.picker}>
<Picker
selectedValue={this.state.schedule}
style={{backgroundColor:'white'}}
onValueChange={(sch) => this.setState({schedule: sch})}>
{this.state.schedules.map((l, i) => {
return <Picker.Item value={l} label={i} key={i} /> })}
</Picker>
</View>
);
}
}
export default connect(mapStateToProps, mapDispatchToProps)(Change);
This isn’t a React Native specific error. You initialized schedules to null so on first render, you try to call .map on null. That’s what is causing your error.
You fetch your data correctly in componentDidMount but that lifecycle method will fire after the initial render.
One common way to fix this is to initialize schedules to an empty array.
First initialise schedules: [] in the state with empty array, not with the null.
Fetching data in componentDidMount() is correct. ComponentDidMount() will be called after the first render of component so you have to update the state in the component from the updated store.
you can check whether props is changing or not in componentWillReceiveProps (depreciated) or in the latest alternative of componentWillReceiveProps method that is getDerivedStateFromProps().
Below is the syntax for both
componentWillReceiveProps(nextProps) {
if (this.props.schedules !== nextProps.schedules) {
this.setState({ schedules: nextProps.schedules });
}
}
static getDerivedStateFromProps(nextProps, prevState){
if (nextProps.schedules !== prevState.schedules) {
return { schedules: nextProps.schedules };
}
else return null; // Triggers no change in the state
}
Make sure your component should connected to store using connect

When to navigate from Authentication SwitchNavigator?

I'm setting up authentication flow in my React Native app, and I can pretty much get everything to work, except I can't figure out the actual proper place to navigate to the logged in stack with this.props.navigation.navigate("Main");.
I have a Switch Navigator at the top of my app rendering the auth stack (its own switch nav) and the main stack:
// App.js
const AppContainer = createAppContainer(
createSwitchNavigator({
Auth: AuthNavigator,
Main: MainTabNavigator,
})
);
export default class App extends Component {
render() {
return (
<Provider store={store}>
<View style={styles.container}>
<AppContainer />
</View>
</Provider>
);
}
}
The login screen is a complete dummy setup for now (it calls a local api and succeeds no matter what). Note the conditional at the start of the render method:
// LoginView.js
class LoginView extends Component {
state = { username: "", password: "" };
handleLogin() {
this.props.login.call(this);
}
render() {
if (this.props.user) {
this.props.navigation.navigate("Main");
return null;
}
return (
<View style={styles.container}>
<Overlay>
<View style={styles.modalContainer}>
<DotIndicator color={"darkgrey"} />
<Text>Logging in...</Text>
</View>
</Overlay>
// ... input components ...
<Button
title="Login"
onPress={this.handleLogin.bind(this)}
/>
</View>
);
}
}
export default connect(
state => ({
isLoading: state.auth.isLoading,
user: state.auth.user,
error: state.auth.error
}),
{ login }
)(LoginView);
Now, this technically works but I get the error Warning: Cannot update during an existing state transition (such as withinrender). Render methods should be a pure function of props and state..
I understand this to be thrown because I'm calling the navigate inside of the render. Makes sense.
I've tried navigating from my login action:
export function login() {
return dispatch => {
dispatch({ type: "LOGIN_START" });
fetch(url) // this will need to be a POST session (not a GET user)
.then(res => res.json())
.then(users => {
dispatch({ type: "LOGIN_SUCCESS", payload: users[0] });
console.log("Login succeeded");
this.props.navigation.navigate("Main");
})
.catch(error => {
console.warn("login failed");
dispatch({ type: "LOGIN_FAILURE", payload: error });
});
};
}
And it works but of course is very wrong in terms of separation of concerns, and this kind of solution also doesn't really stretch as far when I try use it for handling login failure.
I've also thought about doing it in my handleLogin() function (called by my login button), with and without async/await:
async handleLogin() {
await this.props.login.call(this);
console.log("Navigating to main screen");
this.props.navigation.navigate("Main");
}
But the navigation happens before the action finishes and the user is logged in, which is of course unacceptable once the auth flow is real. (Plus, when I do it with async/await, "Login Succeeded" gets logged twice! Beats me). It does feel like I'm on the right track doing it in my handleLogin() function.
Where do I put my navigate call?
I appear to have found something that works, but it takes steps away from Redux, and that makes me slightly uncomfortable.
I've taken all the fetching out of the Redux action and put it right in my LoginView. I'm considering this okay in Redux terms, because the only place the program needs to know about the authorization process and state is in the authorization flow itself (in the LoginView itself, for now). The only thing the store and the rest of the app need to know is the info for the user.
So I've replaced my Redux login action with a simple assignUser action. I've refactored the fetching into performLogin, which I call from the old handleLogin, after posting a state change to activate my "Logging in..." overlay.
// authActions.js
export function assignUser(user) {
return dispatch => {
console.log("user", user);
dispatch({ type: "LOGIN_SUCCESS", payload: user });
};
}
// from LoginView.js
performLogin() {
fetch("http://127.0.0.1:3000/users") // this will need to be a POST session (not a GET user)
.then(res => res.json())
.then(users => {
console.log("Login succeeded");
this.props.assignUser(users[0]);
console.log("Navigating to main screen");
this.props.navigation.navigate("Main");
})
.catch(error => {
console.warn("login failed", error);
this.setState({isLoading: false})
});
}
handleLogin() {
this.setState({ isLoading: true }, this.performLogin);
}
The rest of the LoginView component is the same as in my original post.
I'd still love to know if anyone has any methods for accomplishing this that keep the actions fully within Redux.