I'm attempting to show a simple loading screen component when the data is connecting to firebase (creating a user, or login in). I have set all the indicators with a useState, although when the loading occurs, the screen doesn't pop up.
My register screen:
export function Register({ navigation }: any) {
const [showModal, setShowModal] = useState(false);
const [isLoading, setIsLoading] = useState(false);
useEffect(() => {
if (isLoading) return <Loading />;
}, [isLoading]);
return (
<TouchableWithoutFeedback onPress={Keyboard.dismiss}>
<>
<Modal
visible={showModal}
text={i18n.t('registerModal.title')}
state="success"
description={i18n.t('registerModal.description')}
buttonText={i18n.t('registerModal.goToLogin')}
navigation={undefined}
setShowModal={setShowModal}
onPress={() => {
navigation.navigate('SignIn');
setShowModal(false);
}}
/>
<Container>
<ArrowContainer>
<ArrowTouchable onPress={() => navigation.goBack(null)}>
<ArrowBack width={24} height={24} />
</ArrowTouchable>
</ArrowContainer>
<TitleContainer>
<Title>{i18n.t('signup.title')}</Title>
</TitleContainer>
<Form setShowModal={setShowModal} setIsLoading={setIsLoading} />
<TextContainer>
<Text>{i18n.t('signup.alreadyHaveAccount')}</Text>
<TouchableText onPress={() => navigation.navigate('SignIn')}>
<SignUpText>{i18n.t('signup.singIn')}</SignUpText>
</TouchableText>
</TextContainer>
</Container>
</>
</TouchableWithoutFeedback>
);
}
My Form with sets the loading state:
export function Form({ setShowModal, setIsLoading }: any) {
const {
control,
handleSubmit,
formState: { errors },
} = useForm({
resolver: yupResolver(schema),
});
async function handleUserRegister(data: FormData) {
setIsLoading(true);
const incomingData = await registerWithEmailAndPassword(data);
if (incomingData) {
setIsLoading(false);
setShowModal(true);
}
setIsLoading(false);
}
useEffect(() => {
ToastShowManagement(i18n.t('signup.error'), errors);
}, [errors]);
return (
<Container>
<ControlledInput
name="username"
control={control}
icon="at-sign"
placeholder={i18n.t('signup.username')}
error={errors.username}
/>
<ControlledInput
name="name"
control={control}
icon="user"
placeholder={i18n.t('signup.name')}
error={errors.name}
/>
<ControlledInput
control={control}
name="email"
icon="mail"
placeholder={i18n.t('signup.email')}
keyboardType="email-address"
autoCapitalize="none"
error={errors.email}
/>
<ControlledInput
control={control}
name="password"
icon="lock"
placeholder={i18n.t('signup.password')}
secureTextEntry
error={errors.password}
/>
<ControlledInput
control={control}
name="passwordConfirmation"
icon="lock"
placeholder={i18n.t('signup.confirmPassword')}
secureTextEntry
error={errors.passwordConfirmation}
/>
<PrimaryButton
text={i18n.t('signup.button')}
onPress={handleSubmit(handleUserRegister as any)}
style={{ marginTop: 24 }}
/>
</Container>
);
}
The way to apply useEffect is incorrect
useEffect(() => {
if (isLoading) return <Loading />;
}, [isLoading]);
Does not return the element to main thread, the app.js.
You should take away the useEffect hook
export function Register({ navigation }: any) {
const [showModal, setShowModal] = useState(false);
const [isLoading, setIsLoading] = useState(false);
if (isLoading) return <Loading />
return <>...</>;
}
Related
there! I'm using Firebase for authentication. Everytime, when I try to register a new user, I receive an alert from Firebase, which says that the email address is badly formatted (auth/invalid-email). But if I try to sign in, it works perfectly. I just dont get what the error could be.
RegisterScreen:
const RegisterScreen = ({ navigation }) => {
const [email, setEmail] = useState("");
const [username, setUsername] = useState("");
const [password, setPassword] = useState("");
const register = () => {
auth.createUserWithEmailAndPassword(email, password).then((authUser) => {
authUser.user.updateProfile({
displayName: username
});
})
.catch((error) => {alert(error.message)});
};
return (
<KeyboardAvoidingView behavior="padding" style={styles.container}>
<Text h3 style={styles.title}>Register</Text>
<View style={styles.inputContainer}>
<Input
style={styles.inputText}
placeholder="Email"
placeholderTextColor="#003f5c"
onChangeText={(text) => setEmail(text)}/>
<Input
style={styles.inputText}
placeholder="Benuztername"
placeholderTextColor="#003f5c"
onChangeText={(text) => setUsername(text)}/>
<Input
style={styles.inputText}
placeholder="Password"
secureTextEntry
placeholderTextColor="#003f5c"
onChangeText={(text) => setPassword(text)}/>
<View style={styles.buttonStyle}>
<Button
title="Register"
containerStyle={styles.button}
onPress={register, singIn} />
<Button
title="Back"
type="outline"
containerStyle={styles.button}
onPress={() => navigation.navigate("Login") }/>
</View>
</View>
</KeyboardAvoidingView>
);
};
export default RegisterScreen;
LoginScreen
const LoginScreen = ({ navigation }) => {
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const singIn = () => {
auth.signInWithEmailAndPassword(email, password).catch((error) => {alert(error.message)});
};
useEffect(() => {
const unsubscribe = auth.onAuthStateChanged((authUser) => {
if(authUser){
navigation.replace("Home");
}
});
return unsubscribe;
}, []);
return (
<KeyboardAvoidingView behavior="padding" style={styles.container}>
<Text h3 style={styles.title}>Login</Text>
<View style={styles.inputContainer}>
<Input
style={styles.inputText}
placeholder="Email"
placeholderTextColor="#003f5c"
onChangeText={(text) => setEmail(text)}/>
<Input
style={styles.inputText}
placeholder="Password"
secureTextEntry
placeholderTextColor="#003f5c"
onChangeText={(text) => setPassword(text)}/>
<View style={styles.buttonStyle}>
<Button
title="Login"
containerStyle={styles.button}
onPress={singIn} />
<Button
title="Register"
type="outline"
containerStyle={styles.button}
onPress={() => navigation.navigate("Register") }/>
</View>
</View>
</KeyboardAvoidingView>
);
};
export default LoginScreen;
I have a component that gets a detail of a single post via params.id the component works when I remove all the dependencies on the useEffect but doesnt input the value from the redux state to the component states, when I put the dependencies back it gives me an error of title is undefined, but the redux action returns successfully as I have the data, I think the component loads first before receiving the redux action payload?
const UpdatePost = ({ match, history }) => {
const postId = match.params.id;
const [title, setTitle] = useState('');
const [description, setDescription] = useState('');
const [image, setImage] = useState('');
const dispatch = useDispatch();
const postDetails = useSelector(state => state.postDetails);
const { loading, error, post } = postDetails;
const postUpdate = useSelector(state => state.postUpdate);
const {
loading: loadingUpdate,
error: errorUpdate,
success: successUpdate,
} = postUpdate;
useEffect(() => {
if (successUpdate) {
dispatch({ type: POST_UPDATE_RESET });
dispatch({ type: POST_DETAILS_RESET });
history.push('/edit/post');
} else {
console.log(post, postId);
if (!post.title || post._id !== postId) {
dispatch(listPostDetails(postId));
} else {
setTitle(post.title);
setDescription(post.description);
setImage(post.image);
}
}
}, [post, history, dispatch, postId, successUpdate]);
const submitHandler = e => {
e.preventDefault();
dispatch(
updatePost({
_id: postId,
title,
description,
image,
})
);
};
return (
<MotionBox
exit={{ opacity: 0 }}
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ duration: 1 }}
bgColor={'#eaeaea'}
my="1rem"
border="2px"
borderColor="#eaeaea"
borderRadius="25px"
p={{ base: '2rem 2rem' }}
>
<Heading color={'#435943'} size="lg" pb={'1.5rem'}>
Edit User
</Heading>
{loadingUpdate && <Loader />}
{errorUpdate && <Message status="error">{errorUpdate}</Message>}
{loading && loadingUpdate ? (
<Loader />
) : error ? (
<Message status="error">{error}</Message>
) : (
<form onSubmit={submitHandler}>
<FormControl pb={'1rem'}>
<FormLabel size="md">Edit Post Title</FormLabel>
<Input
borderColor="#d4d6d5"
placeholder="Edit Post Title"
value={title}
onChange={e => setTitle(e.target.value)}
/>
</FormControl>
<FormControl pb={'1rem'}>
<FormLabel size="md">Edit Post Description</FormLabel>
<Textarea
size="md"
value={description}
onChange={e => setDescription(e.target.checked)}
placeholder="Edit Post Descrription"
/>
</FormControl>
<FormControl pb={'1rem'}>
<FormLabel size="md">Edit Post Photo</FormLabel>
<Input
size="md"
placeholder="Edit Post Photo"
value={image}
onChange={e => setImage(e.target.checked)}
/>
</FormControl>
<Button
type="submit"
my={'2rem'}
fontSize={'md'}
fontWeight={600}
color="white"
bg={'green.800'}
_hover={{
background: 'green.600',
}}
_focus={{
outline: 'none',
border: 'none',
}}
>
Update
</Button>
</form>
)}
</MotionBox>
);
};
on App.js, im using useContext to check of user logged in to switch active navigation,
Then, on Login screen after successful login, I got error "Can't perform a React state update on LoginScreen" after the context has changed.
I'm pretty sure it comes from Drawer, because when i try to logout and login back, the drawer opened immediately. but i didn't use any useState to handle login or logout, all my async function only used when handling login or logout not in useState.
you can find my sample video here:
https://vimeo.com/user125707637/review/471649007/2c9dd00d1a
you can see on the video, the drawer opened automatically after the error comes out. each time you login back, the drawer was open and need to close manually. Any idea? or it's a bug.. idk
App.js:
const App = ({navigation, route}) => {
const [user, setUser] = useState();
const [loadingVisible, setLoadingVisible] = useState(true);
const restoreAuth = async () => {
try {
const response = await userApi.getUser();
setUser(response.data);
onFinishLoad();
} catch (error) {
if (error.response.status === 401) {
return onFinishLoad();
}
await storage.removeToken();
setUser(null);
onFinishLoad();
}
};
const onFinishLoad = async () => {
const delay = (ms) => new Promise((res) => setTimeout(res, ms));
await delay(500);
setLoadingVisible(false);
};
useEffect(() => {
let mounted = true;
if (mounted) {
restoreAuth();
}
return () => (mounted = false);
}, []);
return (
<StyleProvider style={getTheme(material)}>
<AuthContext.Provider value={{user, setUser}}>
<LoadingComponent visible={loadingVisible} />
<Container>
<NavigationContainer>
{user ? <AppDrawerNavigation /> : <AuthNavigation />} // checking context changes
</NavigationContainer>
</Container>
</AuthContext.Provider>
</StyleProvider>
);
};
export default App;
Auth Navigator:
const AuthNavigation = () => {
return (
<Stack.Navigator>
<Stack.Screen
name="Login"
component={LoginScreen}
options={{
headerShown: false,
}}
/>
<Stack.Screen name="Register" component={RegisterScreen} />
</Stack.Navigator>
);
};
LoginScreen:
const LoginScreen = ({navigation}) => {
const {user, setUser} = useContext(AuthContext);
const [loadingVisible, setLoadingVisible] = useState(false);
const validationSchema = Yup.object().shape({
email: Yup.string().required().label('E-mail'),
password: Yup.string().required().min(6).label('Password'),
});
const handleSubmit = async ({email, password}, actions) => {
setLoadingVisible(true);
try {
const response = await authApi.login(email, password, 'mobile');
storage.storeToken(response.data.token);
setUser(response.data.user);
setLoadingVisible(false);
} catch (error) {
const statusCode = error.response.status;
const errors = error.response.data;
const fieldErrors = errors?.errors;
const errorMessage = errors?.message;
setLoadingVisible(false);
if (statusCode === 422) {
for (let field in fieldErrors) {
actions.setFieldError(field, fieldErrors[field][0]);
}
} else {
Alert.alert(
'Login Failed',
`${errorMessage}`,
[{text: 'Ok', onPress: () => console.log('ok')}],
{cancelable: false},
);
}
}
};
return (
<Container>
<LoadingComponent visible={loadingVisible} />
<Content padder>
<View>
<Form
initialValues={{
email: '',
password: '',
}}
onSubmit={handleSubmit}
validationSchema={validationSchema}>
<FormField
name="email"
placeholder="E-mail"
keyboardType="email-address"
autoCorrect={false}
/>
<FormField
name="password"
autoCapitalize="none"
autoCorrect={false}
placeholder="Password"
secureTextEntry={true}
/>
<SubmitButton title="LOGIN" />
</Form>
<View style={{marginTop: 20}}>
<Text
style={{
flex: 1,
textAlign: 'center',
textDecorationLine: 'underline',
marginBottom: 10,
}}
onPress={() => navigation.navigate('Register')}>
Create an account
</Text>
<Text
style={{
flex: 1,
textAlign: 'center',
textDecorationLine: 'underline',
}}
onPress={() => navigation.navigate('ForgotPassword')}>
Forgot Password?
</Text>
</View>
</View>
</Content>
</Container>
);
};
Drawer Navigator:
const AppDrawerNavigation = () => {
return (
<Drawer.Navigator
initialRouteName="HomeStack"
drawerContentOptions={{
activeBackgroundColor: colors.primary,
activeTintColor: colors.light,
}}
drawerContent={(props) => <DrawerContent {...props} />}> // Render my custom Drawer
<Drawer.Screen
name="HomeStack"
component={HomeStackNavigation}
options={{
drawerLabel: ({color, focused}) => (
<Text style={{fontSize: !focused ? 14 : 16, color: color}}>
Entries
</Text>
),
drawerIcon: ({color, size, focused}) => (
<AntDesign
name="home"
color={color}
size={!focused ? size : size + 2}
/>
),
}}
/>
</Drawer.Navigator>
);
DrawerContent:
export const DrawerContent = (props) => {
const {user, setUser} = useContext(AuthContext);
const [loadingVisible, setLoadingVisible] = useState(false);
const handleLogout = () => {
Alert.alert(
null,
'Are you sure to logout?',
[
{
text: 'Cancel',
onPress: () => console.log('Cancel Pressed'),
style: 'cancel',
},
{
text: 'OK',
onPress: _logout,
},
],
{cancelable: false},
);
};
//handling logout
const _logout = async () => {
props.navigation.closeDrawer();
setLoadingVisible(true);
try {
const response = await authApi.logout();
await storage.removeToken();
setUser(null); // set user context to null
setLoadingVisible(false);
} catch (error) {
setLoadingVisible(false);
Alert.alert(
'Error',
'Failed to logout',
[
{
text: 'OK',
onPress: console.log('ok'),
},
],
{cancelable: false},
);
}
};
return (
<View style={{flex: 1}}>
<LoadingComponent visible={loadingVisible} />
<DrawerContentScrollView {...props}>
<View style={styles.drawerContent}>
<View style={styles.userInfoSection}>
<View style={{paddingVertical: 20}}>
<Text style={{fontSize: 28}}>SMART</Text>
<Text style={{fontSize: 16}}>Incident Management System</Text>
</View>
</View>
<Drawer.Section style={styles.drawerSection}>
<DrawerItemList {...props} />
</Drawer.Section>
</View>
</DrawerContentScrollView>
<Drawer.Section style={styles.bottomDrawerSection}>
<DrawerItem
icon={({color, size}) => (
<AntDesign name="logout" color={color} size={size} />
)}
label="Sign Out"
onPress={handleLogout}
/>
</Drawer.Section>
</View>
);
};
I have a setCurrentProject call using useState, useEffect hooks api but the modal pops up before the state has been set and it causes the data to throw an error or be blank if i throw in a conditional to check if it's there.
How do I make sure the state changes before the modal opens with the correct prop data
const EditProjectModal = ({modalOpen, handleClose, addTask, currentProject}) => {
const [name, setName] = useState("");
const [startDate, setStartDate] = useState(null);
const [endDate, setEndDate] = useState(null);
const [projectTasks, setProjectTasks] = useState([]);
useEffect(() => {
setName(currentProject.name)
setProjectTasks(currentProject.tasks)
setStartDate(currentProject.startDate)
setEndDate(currentProject.endDate)
}, [currentProject])
return(
<Modal
centered={false}
open={modalOpen}
onClose={handleClose}
>
<Modal.Header>Edit Project</Modal.Header>
<Modal.Content>
<Form onSubmit={handleSubmit}>
<Form.Group widths="equal">
<Form.Field
control={Input}
label="Project"
placeholder="Enter the name of your project..."
required
value={name}
onChange={handleName}
/>
</Form.Group>
<Form.Group widths='equal'>
<Form.Field required>
<label>Start Date</label>
<DatePicker
selected={startDate}
onChange={date => setStartDate(date)}
selectsStart
startDate={startDate}
endDate={endDate}
/>
</Form.Field>
<Form.Field required>
<label>End Date</label>
<DatePicker
selected={endDate}
onChange={date => setEndDate(date)}
selectsEnd
startDate={startDate}
endDate={endDate}
minDate={startDate}
placeholderText="Select end date.."
/>
</Form.Field>
</Form.Group>
<ProjectTasksContainer projectTasks={projectTasks} setProjectTasks={setProjectTasks}/>
<Divider hidden/>
<Button type='submit'>Submit</Button>
</Form>
</Modal.Content>
</Modal>
I've left out some of the change functions etc because that's not important to the state change.
It's being triggered by a button from another component
const handleEdit = () => {
setCurrentProject(project)
handleEditProjClick();
}
edited to add dashboard that uses the editProjectModal
const Dashboard = ({notes, setNotes, tasks, setTasks, goals, setGoals, projects, setProjects}) => {
//============== STATE VARIABLES ===================//
const [projModalOpen, setProjModalOpen] = useState(false);
const [editProjModalOpen, setEditProjModalOpen] = useState(false);
const [currentProject, setCurrentProject] = useState({});
//============== MODALS ===================//
const handleProjModalOpen = () => {
setProjModalOpen(true);
}
const handleProjModalClose = () => {
setProjModalOpen(false);
}
const handleEditProjModalOpen = () => {
setEditProjModalOpen(true);
}
const handleEditProjModalClose = () => {
setEditProjModalOpen(false);
}
//============== RENDERING FUNCTION ===================//
return (
<Fragment>
<Grid columns={2}>
<Grid.Row stretched>
<Grid.Column width={12}>
<Container style={{width: "90%"}}>
<Segment basic>
<Segment textAlign="right">
<ProjectButton handleClick={handleProjModalOpen}/>
<ProjectModal handleClose={handleProjModalClose} modalOpen={projModalOpen} addTask={addTask} addProject={addProject} />
<EditProjectModal handleClose={handleEditProjModalClose} modalOpen={editProjModalOpen} addTask={addTask} currentProject={currentProject} />
</Segment>
</Container>
</Grid.Column>
<Grid.Column width={4}>
<Segment>
<ProgressContainer projects={projects} handleProjClick={handleProjModalOpen} handleEditProjClick={handleEditProjModalOpen} setCurrentProject={setCurrentProject}/>
</Segment>
</Grid.Column>
</Grid.Row>
</Grid>
</Fragment>
)
}
the parent holds the setCurrentProject state and passes it down to that child and also passes down the handleEditProjClick();
What's the best way to handle this issue?
Cheers!
Because editProjModalOpen and currentProject are independent you could get a situation where editProjModalOpen is true and currentProject is {} - you'd need to account for that, e.g. {!isEmpty(currentProject) && <EditProjectModal ... /.>}
This would then guarantee currentProject is set in EditProjectModal and you could just set the initial values in state off the currentProject:
const [name, setName] = useState(currentProject.name);
const [startDate, setStartDate] = useState(currentProject.startDate);
Or if you have to useEffect then just check the state before rendering the modal:
if (!name ) {
return null
}
I'm not great at testing, and new to Jest and Enzyme. I have a Login component that consists of two TextInput components for username and password and a Button component. I am testing each component individually.
I would just like to test that a username and password was returned by onLogin.
Here is the component:
export const onLogin = (user, password) => {
console.log('User', user, 'Password', password)
return [user, password];
};
function Login() {
const [user, setUser] = useState("");
const [password, setPassword] = useState("");
return (
<LoginWrapper>
<Branding brand={brand.brandName} />
<FormWrapper onSubmit={(e) => { e.preventDefault(); onLogin(user, password) }}>
<Stack>
<TextInput
className="username"
type="text"
label="Username"
onChange={e => setUser(e.target.value)}
/>
</Stack>
<Stack>
<TextInput
className="password"
type="password"
label="Password"
onChange={e => {setPassword(e.target.value); console.log('user', user)}}
/>
</Stack>
<Stack padding="0" align="right">
<Button type="submit">Login</Button>
</Stack>
</FormWrapper>
</LoginWrapper>
);
}
export default Login;
My test:
describe("<Login />", () => {
it("renders text input correctly", () => {
const tree = renderer.create(<ThemeProvider theme={themes.default}><Login /></ThemeProvider>).toJSON();
expect(tree).toMatchSnapshot();
});
it("calls onLogin when button clicked", () => {
const onSubmitMock = jest.fn();
const component = Enzyme.mount(
<ThemeProvider theme={themes.default}><Login onSubmit={onSubmitMock} /></ThemeProvider>
);
component.find("input.username").simulate('change', { target: { value: 'myUser' } })
component.find("input.password").simulate('change', { target: { value: 'myPassword' } })
component.find("form").simulate("submit");
console.log("onClickMock.mock", onSubmitMock.mock)
expect(onSubmitMock).toBeCalled()
});
});
Results:
Expected mock function to have been called, but it was not called.
Your testing approach is correct except for:
In your test, you are mocking a callback function and passing it as a property onSubmit to your component. Then you need to call this function from your component when you submit the form.
Instead, you are calling the onLogin function on your component that does not have any
repercussion.
In order to fix this, declare the properties on your component function as a parameter, and call props.onSubmit on your form submit.
function Login(props) {
const [user, setUser] = useState("");
const [password, setPassword] = useState("");
return (
<LoginWrapper>
<Branding brand={brand.brandName} />
<FormWrapper onSubmit={(e) => { e.preventDefault(); props.onSubmit(user, password) }}>
<Stack>
<TextInput
className="username"
type="text"
label="Username"
onChange={(e) => setUser(e.target.value)}
/>
</Stack>
<Stack>
<TextInput
className="password"
type="password"
label="Password"
onChange={(e) => { setPassword(e.target.value) }}
/>
</Stack>
<Stack padding="0" align="right">
<Button type="submit">Login</Button>
</Stack>
</FormWrapper>
</LoginWrapper>
);
}