How to prevent UI flickering while conditionally rendering components? - reactjs

Consider the following code:
const Home = () => {
const [user, setUser] = useState(null);
useEffect(() => {
const unsubscribe = auth.onAuthStateChanged(authUser => {
if(authUser) {
setUser(authUser);
} else {
setUser(null)
}
});
return () => unsubscribe();
}, []);
return (
<div>
{user ? (
<Hero />
) : (
<Login />
)}
</div>
)
}
export default Home
The Login component has all the functions which handles all the Sign Up, Login and Third-Party Authentications using Firebase.
The problems are:
When I reload the page and if the user is already logged in, it shows the component for some time, and then renders the component, which gives a bad UX.
Also, when I sign in using Google or Facebook, again this component is rendered before finally rendering the component.
Please throw some light into this issue. Your help will be highly appreciated!
Edit:
Problem 1 is solved, but problem 2 is not. Here is the relevant code for problem 2:
Login.js
<div style={{ marginBottom: "2%" }}>
<GoogleSignup />
</div>
GoogleSignup.js
import { GoogleLoginButton } from "react-social-login-buttons";
import firebase from "firebase";
import fire from "../fire";
const GoogleSignup = ({ extensionId }) => {
const OnSubmitButton = async () => {
var provider = new firebase.auth.GoogleAuthProvider();
fire
.auth()
.signInWithPopup(provider)
.then((result) => {
const credential = result.credential;
const token = credential.accessToken;
const user = result.user;
})
.catch((error) => {
console.log(error);
});
};
return (
<div>
<GoogleLoginButton
style={{ fontSize: "17px" }}
text={"Continue with Google"}
align={"center"}
onClick={OnSubmitButton}
/>
</div>
);
};
export default GoogleSignup;

These lines:
useEffect(() => {
const unsubscribe = auth.onAuthStateChanged(authUser => {
if(authUser) {
setUser(authUser);
} else {
setUser(null)
}
});
return () => unsubscribe();
}, []);
can be replaced with just:
useEffect(() => auth.onAuthStateChanged(setUser), []);
Next, instead of passing in just null to the useState, pass in current user.
const [user, setUser] = useState(null);
becomes
const [user, setUser] = useState(auth.currentUser);
This results in:
const Home = () => {
const [user, setUser] = useState(auth.currentUser);
useEffect(() => auth.onAuthStateChanged(setUser), []);
return (
<div>
{user ? (
<Hero />
) : (
<Login />
)}
</div>
)
}
export default Home
Personally, I tend to use undefined/null/firebase.auth.User using:
const Home = () => {
const [user, setUser] = useState(() => firebase.auth().currentUser || undefined);
const loadingUser = user === undefined;
useEffect(() => firebase.auth().onAuthStateChanged(setUser), []);
if (loadingUser)
return null; // or show loading icon, etc.
return (
<div>
{user ? (
<Hero />
) : (
<Login />
)}
</div>
)
}
export default Home
After the popup has closed, Firebase Authentication still needs to handle the authentication flow of exchanging the provider's authentication token for a Firebase User token. While this is taking place, you should show some form of loading screen in your component. In the below code sample, I change the "Continue with Google" text to "Signing in..." and disable the onClick events for each button while the sign in process takes place.
import { GoogleLoginButton } from "react-social-login-buttons";
import firebase from "firebase";
import fire from "../fire";
const PROVIDER_ID_GOOGLE = firebase.auth.GoogleAuthProvider.PROVIDER_ID;
const ignoreOnClick = () => {};
const GoogleSignup = ({ extensionId }) => {
const [activeSignInMethod, setActiveSignInMethod] = useState(null);
const [error, setError] = useState(null);
useEffect(() => {
if (activeSignInMethod === null)
return; // do nothing.
let disposed = false, provider;
switch (activeSignInMethod) {
case PROVIDER_ID_GOOGLE:
provider = new firebase.auth.GoogleAuthProvider();
break;
default:
// this is here to help catch when you've added a button
// but forgot to add the provider as a case above
setError("Unsupported authentication provider");
return;
}
fire.auth()
.signInWithPopup(provider)
.then((result) => {
// const credential = result.credential;
// const token = credential.accessToken;
// const user = result.user;
if (!disposed) {
setError(null);
setActiveSignInMethod(null);
}
})
.catch((error) => {
console.error(`Failed to sign in using ${activeSignInMethod}`, error);
if (!disposed) {
setError("Failed to sign in!");
setActiveSignInMethod(null);
}
});
return () => disposed = true; // <- this is to prevent any "updating destroyed component" errors
}, [activeSignInMethod]);
return (
{ error && (<div key="error">{error}</div>) }
<div key="signin-list">
<GoogleLoginButton
style={{ fontSize: "17px" }}
text={
activeSignInMethod == PROVIDER_ID_GOOGLE
? "Signing in..."
: "Continue with Google"
}
align={"center"}
onClick={
activeSignInMethod === null
? () => setActiveSignInMethod(PROVIDER_ID_GOOGLE)
: ignoreOnClick
}
/>
</div>
);
};
export default GoogleSignup;

Related

Can't fetch data on next page (React JS)

import { useState, useEffect } from 'react';
import axios from 'axios'
import { Loading } from './loading';
function News({ pageSize }) {
const [isLoading, setIsLoading] = useState(false)
const [state, setState] = useState({
article: [],
page: 1
}
)
const getUsers = async () => {
setIsLoading(true)
let res = await axios.get(`https://newsapi.org/v2/everything?domains=wsj.com&apiKey=79b02b430c1946cd9c505d3f91d7aec6&page=1&pageSize=${pageSize}`);
setState({article: res.data.articles})
setIsLoading(false)
};
useEffect(() => {
getUsers()
}, [])
const handleNext = async () => {
setIsLoading(true)
let res = await axios.get(`https://newsapi.org/v2/everything?domains=wsj.com&apiKey=79b02b430c1946cd9c505d3f91d7aec6&page=${state.page + 1}&pageSize=${pageSize}`);
setState({article: res.data.articles, page: state.page + 1})
setIsLoading(false)
}
let data = Array.from(state.article)
return (
<div>
<h2>News</h2>
<button onClick={handleNext}>Next</button>
{isLoading && <Loading />}
{!isLoading && data.map((elements) => {
return (
<div key={elements.url} style={{ marginBottom: '2rem' }}>
<div> {elements.description} </div>
<div>{new Date(elements.publishedAt).toGMTString()}</div>
</div>
)
})}
</div>
);
}
export default News;
When I take states separately for data and page, I'm able to display next page's data. But now that I've created one state to manage multiple objects, it displays back first page's data instead of next page's data. I don't know what I'm doing wrong. Pls help me!
Ignore the redundancy.
Try this:
const getUsers = async () => {
setIsLoading(true)
let res = await axios.get(`https://newsapi.org/v2/everything?domains=wsj.com&apiKey=79b02b430c1946cd9c505d3f91d7aec6&page=1&pageSize=${pageSize}`);
setState({...state, article: res.data.articles})
setIsLoading(false)
};

ReactJS Error when using map function, cannot read properties of undefined

I'm trying to make a sport/tinder like app for a school project from a friend of mine. It came together well on my localhost, but for him it was a requirement to host it online. Not really a professional in hosting, but I was a bit familiar with Heroku. I used a client and a server side for my application, so I build the client side and put it into the server side folder. This server side is hosted on the Heroku page. But whenever I try to login, it won't work and I get this error message in my console.
TypeError: Cannot read properties of undefined (reading 'map')
The error says it is caused by this line of code.
const matchedUserIds = matches.map(({user_id}) => user_id)
This is the whole MatchDisplay file that is used in my Dashboard. I'm using a MongoDB for the storage of my users.
import axios from "axios";
import { useEffect, useState } from "react";
import { useCookies } from "react-cookie";
const MatchesDisplay = ({ matches, setClickedUser }) => {
const [matchedProfiles, setMatchedProfiles] = useState(null);
const [cookies, setCookie, removeCookie] = useCookies(null);
const [matched, setMatched] = useState(null);
const matchedUserIds = matches.map(({ user_id }) => user_id);
const userId = cookies.UserId;
const getMatches = async () => {
try {
const response = await axios.get(
"https://[app].herokuapp.com/users",
{
params: { userIds: JSON.stringify(matched()) },
}
);
setMatchedProfiles(response.data);
} catch (error) {
console.log(error);
}
};
useEffect(() => {
getMatches();
}, [matches]);
const filteredMatchedProfiles = matchedProfiles?.filter(
(matchedProfile) =>
matchedProfile.matches.filter(
(profile) => profile.user_id === userId
).length > 0
);
return (
<div className="matches-display">
{filteredMatchedProfiles?.map((match) => (
<div
key={match.user_id}
className="match-card"
onClick={() => setClickedUser(match)}
>
<div className="img-container">
<img
src={match?.url}
alt={match?.first_name + "profile"}
/>
</div>
<h3>{match?.first_name}</h3>
</div>
))}
</div>
);
};
export default MatchesDisplay;
Any help is welcome. If you need more code examples, please reply ;)
EDIT
The ChatContainer that passes the user to the MatchesDisplay.
import ChatHeader from "./ChatHeader";
import MatchesDisplay from "./MatchesDisplay";
import ChatDisplay from "./ChatDisplay";
import { useState } from 'react';
const ChatContainer = ({user}) => {
const [ clickedUser, setClickedUser] = useState(null)
return (
<div className="chat-container">
<ChatHeader user={user}/>
<div>
<button className="option" onClick={() => setClickedUser(null)}>Matches</button>
<button className="option" disabled={!clickedUser}>Chat</button>
<button className="option" >Prices</button>
</div>
{!clickedUser && <MatchesDisplay matches={user.matches} setClickedUser={setClickedUser}/>}
{clickedUser && <ChatDisplay user={user} clickedUser={clickedUser}/>}
</div>
)
}
export default ChatContainer
The Dashboard that passes the user to the Chatcontainer.
import TinderCard from 'react-tinder-card';
import {useEffect, useState} from 'react';
import {useCookies} from 'react-cookie';
import ChatContainer from '../components/ChatContainer'
import axios from "axios";
const Dashboard = () => {
const [user, setUser] = useState(null)
const [genderedUsers, setGenderedUsers] = useState(null)
const [lastDirection, setLastDirection] = useState(null)
const [cookies, setCookie, removeCookie] = useCookies(['user'])
const [matchedUserIds, setMatchedUserIds] = useState(null)
const [filteredGenderedUsers, setFilteredGenderedUsers] = useState(null)
const userId = cookies.UserId
const getUser = async () => {
try {
const response = await axios.get('https://funfit-webpage.herokuapp.com/user', {
params: {userId}
})
return setUser(response.data)
} catch (error) {
console.log(error)
}
}
const getGenderedUsers = async () => {
try {
const response = await axios.get('https://funfit-webpage.herokuapp.com/gendered-users', {
params: {gender: user?.gender_interest}
})
return setGenderedUsers(response.data)
} catch (error) {
console.log(error)
}
}
useEffect(() => {
getUser()
}, [])
useEffect(() => {
setMatchedUserIds(user?.matches.map(({user_id}) => user_id).concat(userId))
if (user) return getGenderedUsers()
}, [user])
useEffect(() => {
if (genderedUsers) {
return setFilteredGenderedUsers(genderedUsers?.filter(
genderedUser => !matchedUserIds.includes(genderedUser.user_id)
))
}
}, [genderedUsers])
const updateMatches = async (matchedUserId) => {
try {
await axios.put('https://funfit-webpage.herokuapp.com/addmatch', {
userId,
matchedUserId
})
return getUser()
} catch (error) {
console.log(error)
}
}
const swiped = (direction, swipedUserId) => {
console.log(direction, swipedUserId)
if (direction === 'right') {
updateMatches(swipedUserId)
}
return setLastDirection(direction)
}
const outOfFrame = (name) => {
console.log(name + ' left the screen!')
}
return (<>
{user && <div className="dashboard">
<ChatContainer user={user}/>
<div className="swipe-container">
<div className="card-container">
{filteredGenderedUsers?.map((genderedUser) =>
<TinderCard
className='swipe'
key={genderedUser.user_id}
onSwipe={(dir) => swiped(dir, genderedUser.user_id)}
onCardLeftScreen={() => outOfFrame(genderedUser.first_name)}>
<div style={{backgroundImage: 'url(' + genderedUser.url + ')'}} className='card'>
<h3>{'Name: ' + genderedUser.first_name} <br/> {'Sport: ' + genderedUser.about}</h3>
</div>
</TinderCard>)}
<div className="swipe-info">
{lastDirection ? <p>You swiped {lastDirection}</p> : <p/>}
</div>
</div>
</div>
</div>}
</>)
}
export default Dashboard

KeyCloak React refreshToken expired token

I want to implement authorization in my client-side application but I've got problem with update Token in React Application with Keycloak.
App.js
import keycloak from "../../keycloak";
const App = () => {
const handleOnEvent = async (event,error) => {
if(event === 'onTokenExpired'){
keycloak.updateToken(300).then(
(response) => {
//I want to update my existing Token
alert("response: ", response )
})
.catch(error => {
console.log("error: ", error)
})
}
}
return (
<>
<ReactKeycloakProvider
authClient={keycloak}
onEvent={(event,error) => handleOnEvent(event,error)}>
<AppRouter/>
</ReactKeycloakProvider>
</>)
}
export default App;
Header
const Header = () => {
const {keycloak,initialized} = useKeycloak()
useEffect(() => {
if(keycloak.authenticated){
alert(JSON.stringify(keycloak))
localStorage.setItem("keycloakToken", keycloak.token); //set keycloak token to localStorag
localStorage.setItem("keycloakRefreshToken", keycloak.refreshToken); // set refresh token
setJWTToken(keycloak.token) //set to axios Authorization Bearer
}
},[keycloak.authenticated])
return(
<>
{
keycloak && !keycloak.authenticated && <UnloggedHeader keycloak={keycloak}/>
}
{
keycloak && keycloak.authenticated && <LoggedHeader keycloak={keycloak}/>
}
</>
)
}
export default Header
UnloggedHeader
function UnloggedHeader({keycloak}){
const signIn = () => {
keycloak.login()
}
return (
<div style={{minWidth: '1100px'}}>
<AppBar position="sticky" color='transparent'>
<Toolbar>
<Button onClick={signIn} variant="contained" color="primary">Login</Button>
<Typography variant="body1" component="h6">Unlogged</Typography>
</Toolbar>
</AppBar>
</div>
);
}
export default UnloggedHeader
LoggedHeader
function LoggedHeader({keycloak}){
let history = useHistory()
const [anchorEl, setAnchorEl] = React.useState(null);
const isMenuOpen = Boolean(anchorEl);
const handleProfileMenuOpen = (event) => {
setAnchorEl(event.currentTarget);
};
const [userInfo,setUserInfo] = useState()
useEffect(() => {
keycloak.loadUserInfo().then(userInfo => {
setUserInfo(userInfo)
localStorage.setItem("username", userInfo.preferred_username); // set username of user
})
},[])
const handleMenuClose = () => {
setAnchorEl(null);
};
const handleUserLogoutClick = () => {
keycloak.logout()
history.push("/")
}
return (
<div style={{minWidth: '1100px'}}>
<AppBar position="sticky" color='transparent'>
<Toolbar>
<Typography variant="body1" component="h6">{userInfo !== undefined ? userInfo.preferred_username : "EMPTY"}</Typography>
<ExpandMoreIcon/>
<Button onClick={handleUserLogoutClick} variant="contained" color="primary">Log out</Button>
</Toolbar>
</AppBar>
{renderMenu}
</div>
);
}
export default LoggedHeader
keycloak.js
import Keycloak from 'keycloak-js'
const keycloakConfig = {
url: 'http://10.192.168.72:8080/auth/',
realm: 'Realm12',
clientId: 'client',
}
const keycloak = new Keycloak(keycloakConfig);
export default keycloak
What I need provide to ReactKeycloakProvider to get new access_token when was expired ?
How based on refreshToken value get accessToken? I don't know which method or endpoint due to get this value. I can't find this kind of problem in network.
Please help me !
You can use event onTokens on Provider
<ReactKeycloakProvider
authClient={keycloak}
onTokens={({ token }) => {
// dispatch(setToken(token));
localStorage.setItem("keycloakToken", token);
}}
<AppRouter/>
</ReactKeycloakProvider>
And to trigger the update method, you can listen the event in your app router like this
export default function AppRouter() {
const { initialized, keycloak } = useKeycloak<KeycloakInstance>();
useEffect(() => {
if (keycloak && initialized) {
keycloak.onTokenExpired = () => keycloak.updateToken(600);
}
return () => {
if (keycloak) keycloak.onTokenExpired = () => {};
};
}, [initialized, keycloak]);
return (
<MyPreferedRouter>
<Switch />
</MyPreferedRouter>
);
}
Is working on #react-keycloak/ssr and i used this implementation with redux to have the token in the store
Don't forget to adapt keycloak.updateToken(600);
600 is number of seconds your minValidity
I made some investigation in this point because I couldn't get new token by refresh token, this is what worked with me
I used Keycloak end point:
https://<yourAuthLink>/auth/realms/<relmName>/protocol/openid-connect/token
with headers object
headers: {'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8'}
and the body will be like that :
body: "client_id"=<clientId>&"grant_type"="refresh_token"&"refresh_token"=<refreshToken>&"client_secret"=<clientSecret>
this will return response which has access_token which you use as token and refresh_token to use it again before expiration time
it is useful link for this type of endpoint and headers
We use this flow
useEffect(() => {
dispatch(keycloak.token);
// and then save it to localStorage
}, [keycloak.token]);
useEffect(() => {
// jast in case
if(!initialized)
return;
if(!keycloak.authenticated)
return;
keycloak.onTokenExpired = () => {
keycloak.updateToken(50);
};
}, [keycloak.authenticated]);
But here I have a question: if the user sleep for a long time and then need to do some API request, so here I have to ask for refreshed token before request
but useKeycloak hook doesn't work in this case

React HOC Using useEffect()

So i am using a HOC for general error handling purposes in react like this:
import React, { useState, useEffect } from 'react'
import Modal from '../../UI/Modal/Modal'
const WithErrorHandler = (WrappedComponent, axios) => {
const NewComponent = props => {
console.log('UseState')
const [error, setError] = useState(null)
console.log('runs')
useEffect(() => {
const req = axios.interceptors.request.use(config => {
console.log('request intercepted')
return config
})
const res = axios.interceptors.response.use(null, error => {
setError(error)
return Promise.reject(error)
})
return () => {
axios.interceptors.request.eject(req)
axios.interceptors.response.eject(res)
}
}, [])
return (
<div>
{console.log('render')}
{error ? (
<Modal clickHandler={() => setError(null)}> {error.message}</Modal>
) : null}
<WrappedComponent {...props} />
</div>
)
}
return NewComponent
}
export default WithErrorHandler
The problem i have run into is that i have a component which fires an axios request in it's useEffect().
When i try to wrap this component with my WithErrorHandler the useEffect of the wrapped component fires first then the useEffect of HOC withErrorHandler runs. This causes the axios request to be made faster than the HOC could register the axios interceptors. Any ideas on how to fix this would be aprreciated.
You can define an intermediate state which prevents from rendering wrapped component.
const WithErrorHandler = (WrappedComponent, axios) => {
const NewComponent = (props) => {
const [ready, setReady] = useState(false); // HERE
console.log("UseState");
const [error, setError] = useState(null);
console.log("runs");
useEffect(() => {
const req = axios.interceptors.request.use((config) => {
console.log("request intercepted");
return config;
});
const res = axios.interceptors.response.use(null, (error) => {
setError(error);
return Promise.reject(error);
});
setReady(true); // HERE
return () => {
axios.interceptors.request.eject(req);
axios.interceptors.response.eject(res);
};
}, []);
if (!ready) return null; // HERE
return (
<div>
{console.log("render")}
{error ? (
<Modal clickHandler={() => setError(null)}> {error.message}</Modal>
) : null}
<WrappedComponent {...props} />
</div>
);
};
return NewComponent;
};
What it does is that it makes sure that axios interceptor is initialized and it is good to render wrapped component.
Instead of if (!ready) return null; you can return a more sensible state from your HOC for instance, if (!ready) return <p>Initializing...</p>
You need an extra render for the NewComponent callback to run, adding a conditional rendering on WrappedComponent should do the trick.
Notice that we set isFirstRender on promise success, change it dependenly on your use case.
const WithErrorHandler = (WrappedComponent, axios) => {
const NewComponent = (props) => {
const [isFirstRender, setIsFirstRender] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
if (isFirstRender) {
const req = axios.interceptors.request.use((config) => {
return config;
});
// Check req success
if (req.isSuccess) { setIsFirstRender(false); }
const res = axios.interceptors.response.use(null, (error) => {
setError(error);
return Promise.reject(error);
});
return () => {
axios.interceptors.request.eject(req);
axios.interceptors.response.eject(res);
};
}
}, [isFirstRender]);
return (
<div>
{error ? (
<Modal clickHandler={() => setError(null)}> {error.message}</Modal>
) : null}
{!isFirstRender && <WrappedComponent {...props} />}
</div>
);
};
return NewComponent;
};

How i can re-render Drawer in React Native?

I'm implementing for the first time the login with Mysql and php in react native, and everything works correctly but the only problem i have is that the content of the Drawer is not updated until i restart the app. I'm implementing AsyncStorage to save if the user is logged or not, if he is logged then return the My Profile button and if it does not return the Sign In button. I have tried this: props.navigation.state.params.refresh(); instead props.navigation.navigate('home'); but does not work for me. Any suggestion? I'm doing something wrong?
Login Function
const login = async() => {
if (email, password) {
signInApi(email, password).then(response => {
if (response != 'error') {
setLogged(true);
props.navigation.navigate('home');
}else if(response === 'error'){
setLogged(false);
}
});
}else{
Alert.alert('Complete Form');
}
}
DrawerContent
export default function DrawerContent(props){
const {navigation} = props;
const [isLogged, setisLogged] = useState(null);
const onChangeScreen = (screen) => {
navigation.navigate(screen);
};
const checkLogged = async () => {
const response = await getLogged();
setisLogged(response);
}
useEffect(() => {
checkLogged();
}
}, []);
renderItem = () => {
if (!isLogged || isLogged === 'false') {
return (
<Button onPress={() => onChangeScreen("signin")}>
Sign In
</Button>
);
}else{
return (
<Button onPress={() => onChangeScreen("profile")}>
My Profile
</Button>
);
}
};
return (
{this.renderItem()}
);
Navigation Drawer
import DrawerContent from './DrawerContent';
const Drawer = createDrawerNavigator();
const DrawerNav = () => {
return (
<Drawer.Navigator initialRouteName="app" drawerContent={(props) => <DrawerContent {...props} />}>
<Drawer.Screen name="app" component={StackNavigation} />
</Drawer.Navigator>
);
};
export default DrawerNav;

Resources