Problem when re-rendering a screen (useEffect/useCallback) - reactjs

On an app I'm working on, I have to change the language of the app based on what the user chose. I have one language selector for non-registered users and another one for registered users.
The code for the selector looks like this
const LanguageSelect: React.FunctionComponent<Props> = (props: Props) => {
const language = useLanguage();
const currentUser = useCurrentUser();
const { updateUser } = props;
const updateLanguage = useCallback((currentUser, language) => {
updateUser(currentUser.set('language', language));
}, [currentUser, language]);
useEffect(() => {
if (currentUser != null) {
try {
updateLanguage(currentUser, language);
} catch (error) {
console.log(error.message);
}
}
}, [language, currentUser, updateLanguage]);
return (
<Select
background={Color.minimalWhite}
useBorderRadius={true}
onValueChange={props.setLocalizationFunction}
items={languageDictionary}
value={languageDictionary.find((lan) => lan.value === language).label}
/>
);
}
The problem is that when a user selects a language when they're not logged in, and then has another language selected when they're logged in (for example, first they select english when not logged in, then french when they're logged in), when the Home screen for a logged in user is rendered, it gets re-rendered almost immediately. I don't know what's going on, I even tried all kinds of stuff to make the useEffect on the languageSelector to run only once, however, the screen gets re-rendered all the time. How can I make the useEffect to run only once whenever the user gets logged in or they change the language by themselves to prevent automatic re-rendering?

Related

React Native: Run actions after succesfully logged in

I'm creating a React Native app in which some actions like adding to favorites require the user to be logged in.
The problem
If a certain action needs authentication, the following flow is executed:
User tab over the favorite button(Protected action)
A modal(screen with presentation: "modal") is rendered to allow the user to enter their credentials
If the user is successfully logged in, the modal is closed and the user is directed to the screen on which it was located(goBack() navigation action).
THE PROBLEM: user needs to press again over the favorite button, the idea is, if the user is successfully logged in, the action (add to favorites) is executed immediately without the user having to perform the action again.
NOTE: I can have different protected actions on the same screen
Question
how can I request the user to log in to perform the action and have the action executed automatically after successful login?
execute the protected action only once, only when the user logs in successfully and the modal is closed, if the user is already authenticated the protected action should not be executed again.
Example flow
function FavoriteScreen({ navigation }) {
const { isAuthenticated } = useAuth();
if (isAuthenticated) {
addFavorite(productId);
} else {
navigation.navigate("LoginScreen");
}
}
Things I've tried
Send a callback through the parameters(called nexAction) from the protected action screen to the login screen, run the callback after successfully log in, and close the modal, but I'm getting non-serializable warning, and this screen implements deep linking, so I cannot ignore the warning as the documentation suggests.
if (isAuthenticated) {
addFavorite();
} else {
navigation.navigate(NavigationScreens.LoginScreen, {
nextAction: addFavorite,
});
}
Use the focus event to run the protected action after the user is successfully logged in and the modal is closed. This approach has some problems, each time user focuses on the screen and is authenticated, the protected action is going to be run, and there may be more than one protected action on the screen, meaning that they will all be executed when the screen is focused.
useEffect(() => {
const unsubscribe = navigation.addListener('focus', () => {
if (isAuthenticated) {
addFavorite();
}
});
return unsubscribe;
}, [isAuthenticated, navigation, addFavorite]);
I think you're on the right path with useEffect. What about storing the user action when clicking a protected action, and then performing the action and clearing the stored value after the user has logged in?
Something like:
const [pendingAction, setPendingAction] = useState();
user clicks addFavorite action, but is not logged in:
setPendingAction({type: 'addFavorite', value: 12});
Then handling the login in the modal, the previous screen will stay mounted and keep its state.
And then calling the pendingAction in the useEffect:
useEffect(() => {
if (isAuth) {
if (pendingAction) {
switch(pendingAction.type) {
// do something
}
setPendingAction(undefined);
}
} else {
// code runs when auth changes to false
}
}, [isAuth, pendingAction, setPendingAction]);
Have you tried an effect?
// Will run once on mount and again when auth state changes
useEffect(() => {
if (isAuth) {
// code runs when auth changes from false to true
} else {
// code runs when auth changes to false
}
}, [isAuth]);
You can't send a function on nav params, but you can send data. You can try parroting the data back from the login screen and performing your one time action like this:
const Fav = ({ route }) => {
const { isAuthenticated } = useAuth();
const {nextAction} = route.params;
useEffect(() => {
if (isAuthenticated) {
// If login navigated back to us with a nextAction, complete it now
if (nextAction === CallbackActions.ADD_FAVORITE) addFavorite();
} else {
navigation.navigate(NavigationScreens.LoginScreen, {
nextAction: CallbackActions.ADD_FAVORITE, // Send some data to the login screen
});
}
}, []);
};
const Login = ({ route }) => {
const {nextAction} = route.params;
// Do login stuff
login().then(() => {
navigation.navigate(NavigationScreens.FavoriteScreen, {
nextAction, // Send back the caller's nextAction
});
});
};

Next.js TypeError although component not rendered

useSession((s) => s.user) will either return an user object or null. On the index page the name of the user should be shown. The page is wrapped in PrivatePageProvider which will redirect the client if the user object is null. So if the user is null, IndexPage should not even be rendered, instead the provider returns null and redirects. However this is not working and a TypeError gets thrown, because IndexPage tries to access name of null. With react-router this approach works, but not with Next. I know I could just use the optional chaining operator, but I want to assume that user is not null if the page is wrapped in PrivatePageProvider.
function PrivatePageProvider({ children }) {
const user = useSession((s) => s.user);
const router = useRouter();
useEffect(() => {
if (!user) {
router.push('/auth/login');
}
}, [user, router]);
return !user ? null : children;
}
function IndexPage() {
const user = useSession((s) => s.user);
return (
<PrivatePageProvider>
<h1>Hello {user.name}</h1>
</PrivatePageProvider>
);
}
Error:
TypeError: Cannot read properties of null (reading 'name')
Why is this error showing up even though IndexPage does not get rendered? How can I get my approach to work without involving the server side?
Why is this error showing up even though IndexPage does not get rendered? > Because it's breaking, you have an error, it won't render.
How can I get my approach to work without involving the server side? > Wrap your code in a useEffect. useEffect will only execute client side so anything being rendered server side will just ignore the effect and wait till it's rendered before executing it. That's because any effects will only execute once the DOM has been rendered. If user is null on the server it will break on user.name unless you move that to an effect.
Additionally, I'm quite confused by what exactly you're trying to do, why is there a useEffect in the child component at all, instead you should be doing that in IndexPage. I'm also not sure what the PrivatePageProvider is for at all, it would make more sense if the page provider was rendering the index page, the other way around. You also shouldn't pass the user as children, instead pass the data prop and render the user in the child page, then you don't need to get the user again.
That being said, looking at your code, it seems to me like you were trying to do something and then got confused/lost along the way by accidentally swapping around your thinking with the way you're using the page and page provider. Perhaps this is what you wanted:
const PrivatePageProvider = ({children}) => {
const user = useSession((s) => s.user)
const router = useRouter()
useEffect(() => {
if (!user) {
router.push('/auth/login')
}
}, [user])
return !user ? <></> : children
}
const IndexPage = () => {
const user = useSession((s) => s.user)
return <h1>Hello {user.name}</h1>
}
So that you can use it like:
<PrivatePageProvider>
<IndexPage/>
</PrivatePageProvider>
<PrivatePageProvider>
<ExamplePage/>
</PrivatePageProvider>
I don't think this approach will work on react-router either. The error is about the context javascript knows when executing your code. To understand better, the JSX tree you are rendering transpiled to plain js looks like this:
function IndexPage() {
const user = useSession((s) => s.user);
return (React.createElement(PrivatePageProvider, null,
React.createElement("h1", null,
"Hello ",
user.name)));
}
When javascript executes a function, it evaluates the arguments first; in this case, when it sees one of them is trying to access a property on a null object(user.name) it throws the TypeError without even calling the function.
The situation is different if you pass the whole user object as a prop to another element
function IndexPage() {
const user = useSession((s) => s.user);
return (
<PrivatePageProvider>
<SomeComponent user={user} />
</PrivatePageProvider>
);
}
It transpiles to
function IndexPage() {
const user = useSession((s) => s.user);
return (React.createElement(PrivatePageProvider, null,
React.createElement(SomeComponent, { user: user })));
}
Here javascript has no idea that you will access user.name inside SomeComponent, hence it continues with the execution without erroring
I suggest using the Context API to provide a non-null user to the components wrapped in your PrivatePageProvider
const UserContext = React.createContext({});
const useUser = () => {
const { user } = useContext(UserContext);
return user;
};
function PrivatePageProvider({ children }: any) {
const user = useSession((s) => s.user);
...
return !user ? null : (
<UserContext.Provider value={{ user }}>{children}</UserContext.Provider>
);
}
function SomeComponent() {
const user = useUser();
return <h1>{user.name}</h1>;
}
function IndexPage() {
return (
<PrivatePageProvider>
<SomeComponent />
</PrivatePageProvider>
);
}
I had a similar problem with my project.
I used nextJS with next-auth that next-auth provided loading state. I don't know how do you get user data but you should get loading state in addition to user and handle authentication using loading. like this :
function PrivatePageProvider({ children }) {
const [session, loading] = useSession();
const router = useRouter();
const isAuthenticate = !!user;
useEffect(() => {
if (loading) return; // Do nothing while loading
if (!isAuthenticate) {
router.push('/auth/login'); // If not authenticated, force log in
}
}, [user, loading]);
if(isAuthenticate) {
return <>children</>
}
// Session is being fetched, or no user.
// If no user, useEffect() will redirect.
return <span>... loading</span>;
}
I put next-auth package address if you like to use it:
next-auth

react native mapStateToProps and React.useEffect render issue

I have a project that needs to show a register modal when the user is null,
it works some times??
I am asking a state element if user exists, then showing the modal, the problem is that when the user is logged in, when the page is shown, sometimes it shows the register modal,
like it renders and then checks if user === null
Is that my problem?
NOTE: on other tabs, this works fine, like it had more time to load the state?
const mapStateToProps = ({ firebase, navigation }) => ({
firebase,
})
function Feed ({ feed, firebase }) {
React.useEffect(
() => navigation.addListener('focus', () =>
{
console.log("aki:: ",firebase.user)
if (firebase.user === null) {
//SHOW MODAL TO INVITE REGISTER
setVisible(true),
navigation.dispatch(navigateToBrowse())
} else {
setVisible(false)
}
}
),
[]
);
It's likely because the firebase.user prop isn't set before this component is rendered. It's impossible to tell without seeing the parent component.
You can block the tree from rendering until firebase.user exists.
Otherwise, you have to differentiate between 1. auth is loading, 2. auth is done loading and user doesn't exist, and 3. auth is done loading and user exists.
I initially thought it was because of how you were handling the navigation side effect, so here's that code anyways:
function Feed({ feed, firebase }) {
const [visible, setVisible] = useState<boolean>(false);
React.useEffect(() => {
const checkForUser = () => {
setVisible(firebase.user === null);
}
// Check for user on all focus events
navigation.addListener('focus', checkForUser);
// Also check for user immediately
checkForUser();
}, []);
React.useEffect(() => {
// Only navigate away when visible gets turned on
if (visible) navigation.dispatch(navigateToBrowse());
}, [visible])
}

ReactJS update data in component when user is logged in

I have dashboard which should show data if a user is logged in and other data if no user is logged in. I already managed to figure out if a user is logged in it is not reflected on the page. It only changes after reloading the page.
This is what I have: An Account object with a userstatus component to hold details of the user. The Account object is placed in a context that is wrapped in the App.js. It also has a getSession function which gets the user details from the authentication mechanism. getSession also updates the userstatus according to the result (logged_in or not_logged_in). Second I have a dashboard component which runs the getSession method and puts the result in the console. Everythings fine. But the render function did not get the changed userstatus.
This is my code (Accounts.js):
export const AccountContext = createContext();
export const Account = {
userstatus: {
loggedinStatus: "not_logged_in",
values: {},
touched: {},
errors: {}
},
getSession: async () =>
await new Promise((resolve, reject) => {
...
}
}),
}
This is the Dashboard.js:
const Dashboard = () => {
const [status, setStatus] = useState();
const { getSession, userstatus } = useContext(AccountContext);
getSession()
.then(session => {
console.log('Dashboard Session:', session);
userstatus.loggedinStatus = "logged_in"
setStatus(1)
})
.catch(() => {
console.log('No Session found.');
userstatus.loggedinStatus = "not_logged_in"
setStatus(0);
});
const classes = useStyles();
return (
<div className={classes.root}>
{userstatus.loggedinStatus}
{status}
{userstatus.loggedinStatus === "logged_in" ? 'User logged in': 'not logged in'}
<Grid
container
spacing={4}
...
I already tried with useState and useEffect, both without luck. The userstatus seems to be the most logical, however, it does not update automatically. How can I reflect the current state in the Dashboard (and other components)?
React only re-renders component when any state change occur.
userstatus is simply a variable whose changes does not reflect for react. Either you should use userstatusas your app state or you can pass it in CreateContext and then use reducers for update. Once any of two ways you use, you would see react's render function reflect the changes in userstatus.
For how to use Context API, refer docs

Strategy to overcome caching of Axios requests?

I've noticed that if I make a GET call to the same API endpoint twice in a row then the previous result appears to get cached. This is causing a problem for the logic of my React component which is a multi-page Wizard.
I define my API request like this:
const [{ data, isLoading, isError }, userNameDoFetch] = useFetch(
null,
{ }
)
The user can then enter a username into an input element and its existence is checked liked this:
const handleBlur = (event) => {
const propName = event.target.name;
const propValue = event.target.value;
if (propValue.length > 0) {
setIsFetching(true);
userNameDoFetch( `${API_ROOT()}account_management/users/name/${propValue}` );
}
}
}
I then have a useEffect that processes the data coming back from the server. It begins like this:
useEffect(() => {
setIsFetching(false);
...
The problem is that the useEffect isn't called if say, the user tabs back into the Username field and then tabs out again. This sets isFetching to true, which disables the Next button in my wizard, leaving the user trapped on that page.
Any thoughts on how to resolve this?

Resources