so, I'm currently working on a MERN app that successfully saves JWT tokens via library localstorage, surviving any refresh attempts (new users show up in the database, etc, the backend is all working as intended).
The issue is, the frontend React app has 'user' set to 'null' by default in the container's state, so that incongruency is what keeps logging users upon out upon re-rendering despite the JWT. I've been stuck on this for over a day now, have tried implementing a variety of possible solutions, have received help from my instructors, etc, nothing is achieving the desired result- does anyone have any advice?
I have attached the code from my container for reference (excuse the messiness, I'm in the middle of being too frustrated with this whole thing to do much about that), Furthermore I also got a bunch of other components and files that interact with my container in some way or other, won't attach them now but if anyone feels that the extra context is needed in order to help then I will do so. Thank you!
import React, { Component } from "react";
import { getItems } from "../services/items";
import Routes from "../routes";
import Header from "../screens/Header";
import { verifyToken } from '../services/auth'
export default class Container extends Component {
constructor(props) {
super(props);
this.state = {
user: null,
items: [],
isLoggedIn: false
};
}
async componentDidMount() {
// const user = await verifyToken();
// if (user) {
try {
const items = await getItems();
this.setState({
items,
isLoggedIn: true
});
}
catch (err) {
console.error(err);
}
}
addItem = item =>
this.setState({
items: [item, ...this.state.items]
});
editItem = (itemId, item) => {
const updateIndex = this.state.items.findIndex(
element => element._id === itemId
),
items = [...this.state.items];
items[updateIndex] = item;
this.setState({
items
});
};
destroyItem = item => {
const destroyIndex = this.state.items.findIndex(
element => element._id === item._id
),
items = [...this.state.items];
if (destroyIndex > -1) {
items.splice(destroyIndex, 1);
this.setState({
items
});
}
};
setUser = user => this.setState({ user });
//verifyUser = user => (localStorage.getItem('token')) ? this.setState({ user, isLoggedIn: true }) : null
clearUser = () => this.setState({ user: null });
render() {
// const token = localStorage.getItem('token');
// console.log(token)
const { user, items } = this.state;
return (
<div className="container-landing">
<Header user={user} />
<main className="container">
<Routes
items={items}
user={user}
setUser={this.setUser}
addItem={this.addItem}
editItem={this.editItem}
destroyItem={this.destroyItem}
clearUser={this.clearUser}
//verifyUser={this.verifyUser}
/>
</main>
</div>
);
}
}
Related
I am learning react.
I have a simple react app sample that :
Fetch users
Once users are fetched, show their name on a Card
What I'd like to do is to expand this sample. Instead of using a simple list of users, I'd like to use a list of pokemons. What I try to do is :
Fetch the list of pokemon and add in state.pokemons
Show the Card with the pokemon name from state.pokemons
From that list, get the URL to fetch the detail of the given pokemon and add in state.pokemonsDetails
From the state.pokemonsDetails, update the Cards list to show the image of the pokemon.
My problem is: I don't even know how to re-render the Cards list after a second fetch.
My question is: How to update the Cards list after the second fetch?
See my code below:
import React from "react";
import CardList from "../components/CardList";
import SearchBox from "../components/SearchBox"
import Scroll from "../components/Scroll"
import './App.css';
class App extends React.Component{
constructor(){
super();
this.state = {
pokemons:[],
pokemonsDetails:[],
searchfield: ''
}
}
getPokemons = async function(){
const response = await fetch('https://pokeapi.co/api/v2/pokemon/?offset=0&limit=20');
const data = await response.json();
this.setState({pokemons:data.results})
}
getPokemonDetails = async function(url){
//fetch function returns a Promise
const response = await fetch(url);
const data = await response.json();
//console.log('getPokemonDetails', data);
this.setState({pokemonsDetails:data});
}
componentDidMount(){
this.getPokemons();
}
onSearchChange = (event) => {
this.setState({searchfield: event.target.value})
}
render(){
const {pokemons, pokemonsDetails, searchfield} = this.state;
if(pokemons.length === 0){
console.log('Loading...');
return <h1>Loading....</h1>
}else if (pokemonsDetails.length === 0){
console.log('Loading details...');
pokemons.map(pokemon => {
return this.getPokemonDetails(pokemon.url);
});
return <h1>Loading details....</h1>
}else{
return(
<div>
<h1>Pokedex</h1>
<SearchBox searchChange={this.onSearchChange}/>
<Scroll>
<CardList pokemons={pokemons}/>
</Scroll>
</div>
);
}
}
}
export default App;
Some remarks :
I can see a problem where my Cards list is first created with state.pokemons, then, I would need to update Cards list with state.pokemonsDetails. The array is not the same.
Second problem, I don't even know how to call the render function after state.pokemonsDetails is filled with the fetch. I set the state, but it looks like render is not called every time
More a question than a remark. The way I update my state in getPokemonDetails might be incorrect. I keep only one detail for one given pokemon. How to keep a list of details? Should I use something else than setState to expand pokemonsDetails array?
You can combine 2 API calls before pokemons state update that would help you to control UI re-renderings better
You can try the below approach with some comments
Side note that I removed pokemonDetails state, so you won't see the loading elements for pokemonDetails as well
import React from "react";
import CardList from "../components/CardList";
import SearchBox from "../components/SearchBox";
import Scroll from "../components/Scroll";
import "./App.css";
class App extends React.Component {
constructor() {
super();
this.state = {
pokemons: [],
searchfield: ""
};
}
getPokemons = async function () {
const response = await fetch(
"https://pokeapi.co/api/v2/pokemon/?offset=0&limit=20"
);
const data = await response.json();
//try to get all pokemon details at once with fetched URLs
const pokemonDetails = await Promise.all(
data.results.map((result) => this.getPokemonDetails(result.url))
);
//map the first and second API response data by names
const mappedPokemons = pokemonDetails.map((pokemon) => {
const pokemonDetail = pokemonDetails.find(
(details) => details.name === pokemon.name
);
return { ...pokemon, ...pokemonDetail };
});
//use mapped pokemons for UI display
this.setState({ pokemons: mappedPokemons });
};
getPokemonDetails = async function (url) {
return fetch(url).then((response) => response.json());
};
componentDidMount() {
this.getPokemons();
}
onSearchChange = (event) => {
this.setState({ searchfield: event.target.value });
};
render() {
const { pokemons, searchfield } = this.state;
if (pokemons.length === 0) {
return <h1>Loading....</h1>;
} else {
return (
<div>
<h1>Pokedex</h1>
<SearchBox searchChange={this.onSearchChange} />
<Scroll>
<CardList pokemons={pokemons} />
</Scroll>
</div>
);
}
}
}
export default App;
Sandbox
If you want to update pokemon details gradually, you can try the below approach
import React from "react";
import CardList from "../components/CardList";
import SearchBox from "../components/SearchBox";
import Scroll from "../components/Scroll";
import "./App.css";
class App extends React.Component {
constructor() {
super();
this.state = {
pokemons: [],
searchfield: ""
};
}
getPokemons = async function () {
const response = await fetch(
"https://pokeapi.co/api/v2/pokemon/?offset=0&limit=20"
);
const data = await response.json();
this.setState({ pokemons: data.results });
for (const { url } of data.results) {
this.getPokemonDetails(url).then((pokemonDetails) => {
this.setState((prevState) => ({
pokemons: prevState.pokemons.map((pokemon) =>
pokemon.name === pokemonDetails.name
? { ...pokemon, ...pokemonDetails }
: pokemon
)
}));
});
}
};
getPokemonDetails = async function (url) {
return fetch(url).then((response) => response.json());
};
componentDidMount() {
this.getPokemons();
}
onSearchChange = (event) => {
this.setState({ searchfield: event.target.value });
};
render() {
const { pokemons, searchfield } = this.state;
if (pokemons.length === 0) {
return <h1>Loading....</h1>;
} else {
return (
<div>
<h1>Pokedex</h1>
<SearchBox searchChange={this.onSearchChange} />
<Scroll>
<CardList pokemons={pokemons} />
</Scroll>
</div>
);
}
}
}
export default App;
Sandbox
Side note that this approach may cause the performance issue because it will keep hitting API for fetching pokemon details multiple times and updating on the same state for UI re-rendering
Everything auth-wise is working fine. I even have a loading state setup so that the loader shows until the state is changed, but I still get this flickering on reload. This flickering only happens with Supabase. I was using the Firebase version before and it worked perfectly with my code.
Here is a video for reference: https://imgur.com/a/5hywXj5
Edit: Updated code to current version
export default function Navigation() {
const { user, setUser } = useContext(AuthenticatedUserContext);
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
const session = supabase.auth.session();
setUser(session?.user ?? null);
const { data: listener } = supabase.auth.onAuthStateChange((_: any, session: any) => {
setUser(session?.user ?? null);
});
setIsLoading(false);
return () => {
listener?.unsubscribe();
};
}, []);
if (isLoading) {
return (
<View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
<ActivityIndicator color={Theme.colors.purple} size="large" />
</View>
);
}
return (
<NavigationContainer linking={LinkingConfiguration}>{user ? <AppStack /> : <AuthStack />}</NavigationContainer>
);
}
To recap for others, onAuthStateChange will not execute on first page load so you are triggering it using the getUserAuthStatus async function. However session() function is not async and will immediately return a result of null if there is no user session, or return a session that has been stored in localStorage.
In this case the result of the getUserAuthStatus will always return null. Then onAuthStateChange will trigger with the SIGNED_IN event and a session which will then set the user.
Furthermore the onAuthStateChange function should be registered before you perform the session step so as to capture any events triggered. In the current form an event may be triggered directly after the session() call but before the handler is registered.
So to recap the rendering steps will be:
Step 1
isLoading: true
user: null
Step 2
isLoading: false
user: null
Step 3
isLoading false
user: {...}
So far as I can tell, using session directly without thinking it's async will do the trick.
Ok, Supabase has released some updates since I first asked this question. Here is how I am now able to stop flickering when loading the application.
First, we need to set up our AuthContext for our application. Be sure to wrap your App.tsx with the <AuthContextProvider>.
AuthContext.tsx
import React, { createContext, useContext, useEffect, useState } from 'react';
import { Session, User } from '#supabase/supabase-js';
import { supabase } from '../config/supabase';
export const AuthContext = createContext<{ user: User | null; session: Session | null }>({
user: null,
session: null,
});
export const AuthContextProvider = (props: any) => {
const [userSession, setUserSession] = useState<Session | null>(null);
const [user, setUser] = useState<User | null>(null);
useEffect(() => {
supabase.auth.getSession().then(({ data: { session } }) => {
setUserSession(session);
setUser(session?.user ?? null);
});
const { data: authListener } = supabase.auth.onAuthStateChange(async (event, session) => {
console.log(`Supabase auth event: ${event}`);
setUserSession(session);
setUser(session?.user ?? null);
});
return () => {
authListener.subscription;
};
}, []);
const value = {
userSession,
user,
};
return <AuthContext.Provider value={value} {...props} />;
};
export const useUser = () => {
const context = useContext(AuthContext);
if (context === undefined) {
throw new Error('useUser must be used within a AuthContextProvider.');
}
return context;
};
Now, if you're using React Navigation like me we need to check if we have a valid user to send them to the logged-in home screen. Here's how I do it.
Navigation.tsx
export default function Navigation() {
const { user } = useUser();
return (
<NavigationContainer linking={LinkingConfiguration}>
{user ? <AppStackNavigator /> : <AuthStackNavigator />}
</NavigationContainer>
);
}
When I make a request to an API and setting the state to the results from the Axios request it still shows up null. I am using React useState and setting the results from the request and wanting to check to see if its coming through correctly and getting the right data its still resulting into null. The request is correct but when I use .then() to set the state that is the issue I am having.
Below is the component that I am building to make the request called Details.js (first code block) and the child component is the DetailInfo.js file (second code block) that will be displaying the data. What am I missing exactly or could do better when making the request and setting the state correctly display the data?
import React, {useEffect, useState} from 'react';
import { Col, Container, Row } from 'react-bootstrap';
import axios from 'axios';
import { getCookie } from '../utils/util';
import DetailInfo from '../components/DetailInfo';
import DetailImage from '../components/DetailImage';
const Details = () => {
const [ countryData, setCountryData ] = useState(null);
let country;
let queryURL = `https://restcountries.eu/rest/v2/name/`;
useEffect(() => {
country = getCookie('title');
console.log(country);
queryURL += country;
console.log(queryURL);
axios.get(queryURL)
.then((res) => {
console.log(res.data[0])
setCountryData(res.data[0]);
})
.then(() => {
console.log(countryData)
}
);
}, [])
return (
<>
<Container className="details">
<Row>
<Col sm={6}>
<DetailImage />
</Col>
<Col sm={6}>
<DetailInfo
name={countryData.name}
population={countryData.population}
region={countryData.region}
subRegion={countryData.subRegion}
capital={countryData.capital}
topLevelDomain={countryData.topLevelDomain}
currencies={countryData.currencies}
language={countryData.language}
/>
</Col>
</Row>
</Container>
</>
)
}
export default Details;
The child component below......
import React from 'react';
const DetailInfo = (props) => {
const {name, population, region, subRegion, capital, topLevelDomain, currencies, language} = props;
return (
<>detail info{name}{population} {region} {capital} {subRegion} {topLevelDomain} {currencies} {language}</>
)
}
export default DetailInfo;
Ultimately, the problem comes down to not handling the intermediate states of your component.
For components that show remote data, you start out in a "loading" or "pending" state. In this state, you show a message to the user saying that it's loading, show a Spinner (or other throbber), or simply hide the component. Once the data is retrieved, you then update your state with the new data. If it failed, you then update your state with information about the error.
const [ dataInfo, setDataInfo ] = useState(/* default dataInfo: */ {
status: "loading",
data: null,
error: null
});
useEffect(() => {
let unsubscribed = false;
fetchData()
.then((response) => {
if (unsubscribed) return; // unsubscribed? do nothing.
setDataInfo({
status: "fetched",
data: response.data,
error: null
});
})
.catch((err) => {
if (unsubscribed) return; // unsubscribed? do nothing.
console.error('Failed to fetch remote data: ', err);
setDataInfo({
status: "error",
data: null,
error: err
});
});
return () => unsubscribed = true;
}, []);
switch (dataInfo.status) {
case "loading":
return null; // hides component
case "error":
return (
<div class="error">
Failed to retrieve data: {dataInfo.error.message}
</div>
);
}
// render data using dataInfo.data
return (
/* ... */
);
If this looks like a lot of boiler plate, there are useAsyncEffect implementations like #react-hook/async and use-async-effect that handle it for you, reducing the above code to just:
import {useAsyncEffect} from '#react-hook/async'
/* ... */
const {status, error, value} = useAsyncEffect(() => {
return fetchData()
.then((response) => response.data);
}, []);
switch (status) {
case "loading":
return null; // hides component
case "error":
return (
<div class="error">
Failed to retrieve data: {error.message}
</div>
);
}
// render data using value
return (
/* ... */
);
Because state only update when component re-render. So you should put console.log into useEffect to check the new value:
useEffect(() => {
country = getCookie('title');
console.log(country);
queryURL += country;
console.log(queryURL);
axios.get(queryURL).then(res => {
console.log(res.data[0]);
setCountryData(res.data[0]);
});
}, []);
useEffect(() => {
console.log(countryData);
}, [countryData]);
useState does reflecting its change immediately.
I think that it would be probably solved if you set countryData to second argument of useEffect.
useEffect(() => {
country = getCookie('title');
console.log(country);
queryURL += country;
console.log(queryURL);
axios.get(queryURL)
.then((res) => {
console.log(res.data[0])
setCountryData(res.data[0]);
})
.then(() => {
console.log(countryData)
}
);
}, [countryData])
The issue is, as samthecodingman, pointed out, an issue of intermediate data. Your component is being rendered before the data is available, so your child component needs to re-render when its props change. This can be done via optional chaining, an ES6 feature.
import React, { useEffect, useState } from "react";
import DetailInfo from "./DetailInfo";
import { Col, Container, Row } from "react-bootstrap";
import axios from "axios";
const Details = () => {
const [countryData, setCountryData] = useState({});
let country = "USA";
let queryURL = `https://restcountries.eu/rest/v2/name/`;
useEffect(() => {
console.log(country);
queryURL += country;
console.log(queryURL);
axios
.get(queryURL)
.then((res) => {
console.log(res.data[0]);
setCountryData(res.data[0]);
})
.then(() => {
console.log(countryData);
});
}, []);
return (
<Container className="details">
<Row>
<Col sm={6}>
<DetailInfo
name={countryData?.name}
population={countryData?.population}
region={countryData?.region}
subRegion={countryData?.subRegion}
capital={countryData?.capital}
language={countryData?.language}
/>
</Col>
<Col sm={6}></Col>
</Row>
</Container>
);
};
export default Details;
Checkout my Codesandbox here for an example.
I'm working on a SPA with data fetch from the Star Wars API.
In the characters tab of the project, the idea is to display the characters per page and you can click next or prev to go to page 2, 3, etc. That works, but! the character names flicker everytime the page changes, it doesn't happen after the first click, but if you keep going, it happens more and more.
I tried to fixed it by cleaning the state before rendering again, but it's not working. The data is first fetched after the component mounts, then when the btn is clicked I use the componentwillupdate function to update the character component.
You can see the component here: https://github.com/jesusrmz19/Star-Wars-App/blob/master/src/components/Characters.js
And the live project, here: https://starwarsspa.netlify.app/#/Characters
see if this solves your problem
class Characters extends React.Component {
constructor(props) {
super(props);
this.state = {
error: null,
isLoaded: false,
webPage: "https://swapi.dev/api/people/?page=",
pageNumber: 1,
characters: [],
};
this.fetchHero = this.fetchHero.bind(this);
}
async fetchHero(nextOrPrev) {
//nextOrPrev values-> 0: initial 1: next page -1:prev page
let pageNum = this.state.pageNumber + nextOrPrev;
try {
const response = await fetch(this.state.webPage + pageNum);
const data = await response.json();
const characters = data.results;
this.setState({
pageNumber: pageNum,
characters,
isLoaded: true,
});
} catch (error) {
this.setState({
pageNumber: pageNum,
isLoaded: true,
error,
});
}
}
componentDidMount() {
this.fetchHero(0);
}
/*you don't need this-> componentDidUpdate(prevState) {
if (this.state.pageNumber !== prevState.pageNumber) {
const fetchData = async () => {
const response = await fetch(
this.state.webPage + this.state.pageNumber
);
const data = await response.json();
this.setState({ characters: data.results });
};
fetchData();
}
}*/
getNextPage = () => {
if (this.state.pageNumber < 9) {
this.fetchHero(1);
}
};
getPrevPage = () => {
if (this.state.pageNumber > 1) {
this.fetchHero(-1);
}
};
render(
// the rest of your code
)
}
I took a look at your Characters component and spent some time refactoring it to React Hooks to simplify it.
Instead of making multiple requests, I created a function that does one request and assigns a state when needed. Plus, I would use status instead of isLoading so that you can render the content better based on the status that your component is currently on.
Also, I used useEffect which does the same thing that componentDidMount + componentDidUpdate does. I then used the request function in there and added pageNumber as a dependency which means that the request will be made only when the pageNumber changes. In your case, it will change only if you press previous or next buttons.
I also simplified your getPrev and getNext page functions. Finally, I rendered the page content based on the status.
I've pulled your project down and run it locally and I cannot see the flickers anymore. Instead, it shows loading screen while it fetches the data and then nicely renders what you need.
Hope that helps. I would also advise starting looking at React Hooks as they make using React way simpler and it's a modern way to develop React applications as well. I've refactored your Characters component and if you want then use this as an example of how to refactor from Class Components to React Hooks.
import React, { useState, useEffect } from "react";
import Character from "./Character";
const baseUrl = "https://swapi.dev/api/people/?page=";
const Characters = () => {
const [state, setState] = useState({
characters: [],
pageNumber: 1,
status: "idle",
error: null,
});
const { characters, pageNumber, status, error } = state;
const fetchData = async (pageNumber) => {
setState((prevState) => ({
...prevState,
status: "fetching",
}));
await fetch(`${baseUrl}${pageNumber}`).then(async (res) => {
if (res.ok) {
const data = await res.json();
setState((prevState) => ({
...prevState,
characters: data.results,
status: "processed",
}));
} else {
setState((prevState) => ({
...prevState,
characters: [],
status: "failed",
error: "Failed to fetch characters",
}));
}
});
};
useEffect(() => {
fetchData(pageNumber);
}, [pageNumber]);
const getNextPage = () => {
if (pageNumber !== 9) {
setState((prevState) => ({
...prevState,
pageNumber: prevState.pageNumber + 1,
}));
}
};
const getPrevPage = () => {
if (pageNumber !== 1) {
setState((prevState) => ({
...prevState,
pageNumber: prevState.pageNumber - 1,
}));
}
};
return (
<div>
{status === "failed" ? (
<div className="charact--container">
<div className="loading">{error}</div>
</div>
) : null}
{status === "fetching" ? (
<div className="charact--container">
<div className="loading">Loading...</div>
</div>
) : null}
{status === "processed" ? (
<div className="charact--container--loaded">
<h1>Characters</h1>
<button onClick={getPrevPage}>Prev Page</button>
<button onClick={getNextPage}>Next Page</button>
<ul className="characters">
{characters.map((character, index) => (
<Character details={character} key={index} index={index} />
))}
</ul>
</div>
) : null}
</div>
);
};
export default Characters;
I'm new in using React Context and Hooks in my project. Currently I'm facing a problem when my item in the component doesn't display on the screen when it initially load but the item does display on the screen when I clicked on some button.
I did do some debugging using console.logand in the console, it did shows my data, but on the screen, it shows nothing. The weird part is, when I clicked on any button on the screen, it finally show something on the screen.
Here is my code, in the OrderContext, I get all my data from Firestore.
//OrderContextProvider.js
import React, { createContext, useState, useEffect } from "react";
import Firebase from "../component/misc/firebase";
export const OrderContext = createContext();
const OrderContextProvider = props => {
const [userLocation, setUserLocation] = useState({
shop: "XXXXXXX"
});
const [food] = useState([]);
useEffect(() => {
userLocation.shop != null
? Firebase.firestore()
.collection("restaurants")
.doc(userLocation.shop)
.collection("foods")
.get()
.then(function(querySnapshot) {
querySnapshot.forEach(function(doc) {
food.push(doc.data());
});
})
.catch(function(error) {
console.log("Error getting documents: ", error);
})
: console.log("User location unknown, please insert the location!");
}, [food]);
return (
<OrderContext.Provider value={{ userLocation, food }}>
{props.children}
</OrderContext.Provider>
);
};
export default OrderContextProvider;
and in Foods component, I tried get food from OrderContext and display <Foods/> in <Home/>
//foods.js
import React, { useEffect, useContext } from "react";
import { OrderContext } from "../../context/OrderContextProvider";
import { Grid } from "#material-ui/core";
const Foods = () => {
const { food } = useContext(OrderContext);
useEffect(() => {
console.log(food);
}, []);
return food.map((foods, index) => {
return (
<Grid key={index} item xs={6} sm={6} md={3} xl={3} className="food">
{foods.name}
</Grid>
);
});
};
export default Foods;
//foods.js
<Grid container className="container">
<Foods/>
</Grid>
May I know what is my mistake or what I missed out here?
Sorry for my bad English and thanks for reading
It's still unclear as to where/how you are using the OrderContextProvider component. But, one possible problem I see in the code is the way you are updating the "food" array. The way you are pushing items into it won't necessarily trigger the updates since you are not updating the state. You might want to do something like -
const OrderContextProvider = props => {
....
const [food, setFood] = useState([]);
useEffect(() => {
...
.then(function(querySnapshot) {
let newFoods = [];
querySnapshot.forEach(function(doc) {
newFoods.push(doc.data());
});
setFood([...food, ...newFoods]);
})
...
});
...
};
const food = [];
useEffect(() => {
userLocation.shop != null
? Firebase.firestore()
.collection("restaurants")
.doc(userLocation.shop)
.collection("foods")
.get()
.then(function(querySnapshot) {
querySnapshot.forEach(function(doc) {
food.push(doc.data());
});
})
.catch(function(error) {
console.log("Error getting documents: ", error);
})
: console.log("User location unknown, please insert the location!");
},[food]);
You have not passed second argument to the useEffect that tells the hook to run for the first time it loads.
I guess this is the reason why it's not showing the data on the first load.