I'm trying to pass data to the component, that i received from the API. I am using 'createAsyncThunk' to save it in the state, than when trying to get my data, get error "undefined". I understand that it happens, cause it's need some time to get data from API, but how i can force component "waiting"? What is wrong with my code?
Here is my code:
Step 1: Gettind data from API, filtered it and push it in state.
import { generateRandom } from "../helpers/randomInt";
const API_URL = "https://akabab.github.io/superhero-api/api/all.json";
export const fetchHeroes = createAsyncThunk(
"data_slice/fetchHeroes",
async function (_, { rejectWithValue }) {
try {
const res = await fetch(API_URL);
if (!res.ok) {
throw new Error("Could not fetch cart data!");
}
const data = await res.json();
const marvel_heroes = data.filter(
(item) => item.biography.publisher == "Marvel Comics"
);
const dark_horse_heroes = data.filter(
(item) => item.biography.publisher == "Dark Horse Comics"
);
const dc_heroes = data.filter(
(item) => item.biography.publisher == "DC Comics"
);
const filtered_data = [
...marvel_heroes,
...dark_horse_heroes,
...dc_heroes,
];
const heroesData = [];
for (let index = 0; index < 49; index++) {
const item = filtered_data[generateRandom(0, 439)];
heroesData.push(item);
}
const main_data = [filtered_data, heroesData];
return main_data;
} catch (error) {
return rejectWithValue(error.message);
}
}
);
const heroesSlice = createSlice({
name: "data_slice",
initialState: { heroes_data: [], isLoading: null, error: null },
extraReducers: {
[fetchHeroes.pending]: (state) => {
state.isLoading = true;
},
[fetchHeroes.fulfilled]: (state, action) => {
state.heroes_data = action.payload;
state.isLoading = false;
},
[fetchHeroes.rejected]: (state) => {
state.isLoading = false;
state.error = "Something go wrong!";
alert("aaa");
},
},
});
export default heroesSlice;
Step 2: Firing (using dispatch) fetch function "fetchHeroes" in 'App.js' with 'UseEffect' to get data when app starting
import { Fragment, useState, useEffect } from "react";
import { useSelector, useDispatch } from "react-redux";
import { fetchHeroes } from "./store/heroes-slice";
import { Routes, Route } from "react-router-dom";
import Main from "./pages/Main";
import Heroes from "./pages/Heroes";
import Hero_page from "./pages/Hero_page";
import LoginModal from "./components/LoginModal";
import RegisterModal from "./components/RegisterModal";
function App() {
const [scrollY, setScrollY] = useState(0);
const isLoginModal = useSelector((state) => state.modal.isLoginModal);
const isRegisterModal = useSelector((state) => state.modal.isRegisterModal);
const dispatch = useDispatch();
function logit() {
setScrollY(window.scrollY);
console.log(new Date().getTime());
}
useEffect(() => {
function watchScroll() {
window.addEventListener("scroll", logit);
}
watchScroll();
return () => {
window.removeEventListener("scroll", logit);
};
});
useEffect(() => {
dispatch(fetchHeroes());
}, [dispatch]);
return (
<Fragment>
{isRegisterModal && <RegisterModal></RegisterModal>}
{isLoginModal && <LoginModal></LoginModal>}
<Routes>
<Route path="/" element={<Main />} />
<Route path="/heroes" exact element={<Heroes scroll={scrollY} />} />
<Route path="/heroes/:heroId" element={<Hero_page />}></Route>
</Routes>
</Fragment>
);
}
export default App;
Step 3: I am trying to recieve data from state(heroes_fetched_data) using 'usSelector', but when trying parce it through 'map', get error 'undefined'
import classes from "./Heroes.module.css";
import Header from "../components/Header";
import Footer from "../components/Footer.js";
import Hero_card from "../components/Hero_card";
import { useSelector } from "react-redux";
import { Link } from "react-router-dom";
export default function Heroes(props) {
const heroes_fetched_data = useSelector((state) => state.heroes.heroes_data);
const loadingStatus = useSelector((state) => state.heroes.isLoading);
console.log(heroes_fetched_data);
const heroes_cards = heroes_fetched_data[1].map((item, i) => (
<Link to={`/heroes/${item.id}`} key={item.id + Math.random()}>
<Hero_card
key={i}
img={item.images.lg}
name={item.name}
publisher={item.biography.publisher}
/>
</Link>
));
return (
<div className={classes.main}>
<Header scroll={props.scroll} />
{!loadingStatus && (
<section className={classes.heroes}>
<ul className={classes.ully} id="heroes">
{heroes_cards}
</ul>
</section>
)}
{loadingStatus && <p>Loading...</p>}
<Footer />
</div>
);
}
Because the fetch is asynchronous, you cannot assume that heroes_fetched_data inside your Heroes component will have the data when the component first renders. You need to check whether this data is present before attempting to use it. If it's not yet present, the component should render an alternate "loading" state. When the fetch completes, your component should re-render automatically, at which point heroes_fetched_data will have the data you want and you can proceed.
Roughly, you want something like this pseudocode:
export default function Heroes(props) {
const heroes_fetched_data = useSelector((state) => state.heroes.heroes_data);
const loadingStatus = useSelector((state) => state.heroes.isLoading);
if (!heroes_fetched_data) {
return <p>{loadingStatus}</p>;
}
const heroes_cards = heroes_fetched_data[1].map((item, i) => (
// ...
);
// proceed as normal
}
Related
I'm building a simple venue review app using react/redux toolkit/firebase.
The feature VenueList.js renders a list of venues. When the user clicks on a venue, it routes them to Venue.js page which renders information about the specific venue clicked on.
Here's the problem: Venue.js renders on the first page load, but crashes when I try to refresh the page.
After some investigating I found that in Venues.js, the useSelector hook returned the correct state on first load, and then an empty array upon refresh:
Intial page load:
On page refresh
Why is this happeing and how can I fix this so that the page renders in all circumstances?
Here's Venue.js
import { useParams } from "react-router-dom";
import { useSelector } from "react-redux";
import AddReview from "../../components/AddReview";
import Reviews from "../../components/Reviews";
const Venue = () => {
const { id } = useParams();
const venues = useSelector((state) => state.venues);
const venue = venues.venues.filter((item) => item.id === id);
console.log(venues)
const content = venue.map((item) => (
<div className="venue-page-main" key = {item.name}>
<h2>{item.name}</h2>
<img src={item.photo} alt = "venue"/>
</div>
));
return (
<>
{content}
<AddReview id = {id}/>
{/* <Reviews venue = {venue}/> */}
</>
);
};
export default Venue;
The list of venues in VenueList.js
import { Link } from "react-router-dom";
import { useEffect } from "react";
import { fetchVenues } from "./venueSlice";
import { useSelector,useDispatch } from "react-redux";
const VenueList = () => {
const dispatch = useDispatch();
useEffect(() => {
dispatch(fetchVenues());
}, [dispatch]);
const venues = useSelector((state) => state.venues);
const content = venues.venues.map((venue) => (
<Link to={`/venue/${venue.id}`} style = {{textDecoration: "none"}} key = {venue.name}>
<div className="venue-item">
<h2>{venue.name}</h2>
<img src={venue.photo} />
</div>
</Link>
));
return (
<div className="venue-list">
{content}
</div>
);
};
export default VenueList;
And here's the slice venueSlice.js controlling all the API calls
import { createSlice,createAsyncThunk } from "#reduxjs/toolkit";
import { collection,query,getDocs,doc,updateDoc,arrayUnion, arrayRemove, FieldValue } from "firebase/firestore";
import { db } from "../../firebaseConfig";
const initialState = {
venues: []
}
export const fetchVenues = createAsyncThunk("venues/fetchVenues", async () => {
try {
const venueArray = [];
const q = query(collection(db, "venues"));
const querySnapshot = await getDocs(q);
querySnapshot.forEach((doc) =>
venueArray.push({ id: doc.id, ...doc.data() })
);
return venueArray;
} catch (err) {
console.log("Error: ", err);
}
});
export const postReview = createAsyncThunk("venues/postReview", async (review) => {
try {
const venueRef = doc(db,"venues",review.id)
await updateDoc(venueRef, {
reviews: arrayUnion({
title:review.title,
blurb:review.blurb,
reviewId:review.reviewId })
})
} catch (err) {
console.log('Error :', err)
}
})
export const deleteReview = createAsyncThunk("venues/deleteReview", async (review) => {
const newReview = {blurb:review.blurb, title: review.title, reviewId: review.reviewId}
try {
const venueRef = doc(db,"venues",review.id)
await updateDoc(venueRef, {
reviews: arrayRemove(newReview)
})
} catch (err) {
console.log('Error: ', err)
}
})
const venueSlice = createSlice({
name: "venues",
initialState,
reducers: {},
extraReducers(builder) {
builder
.addCase(fetchVenues.fulfilled, (state, action) => {
state.venues = action.payload;
})
},
});
export default venueSlice.reducer
I think this is what is going on:
First time you load this page, you first visit the list of venues so the call to fetch them is made and the venues are stored to redux. Then when you visit a specific venue, the list exists so the selector always returns data.
dispatch(fetchVenues());
When you refetch the page you are in the /venue/${venue.id} route.
The dispatch to fetch the list hasn't been called and so you get the errors you mention.
There are a couple of ways to fix your issue
Fetch the venues if the data are not available. In Venue.js do something like:
const Venue = () => {
const { id } = useParams();
const venues = useSelector((state) => state.venues) || [];
const venue = venues.venues.filter((item) => item.id === id);
useEffect(() => {
if(venues?.length === 0) {
dispatch(fetchVenues());
}
}, [dispatch, venues, id]);
console.log(venues)
// You need to check if the venue exists, otherwise your code will throw errors
if(!venue) {
return <div>Some loader or error message<div/>
}
const content = venue.map((item) => (
<div className="venue-page-main" key = {item.name}>
<h2>{item.name}</h2>
<img src={item.photo} alt = "venue"/>
</div>
));
return (
<>
{content}
<AddReview id = {id}/>
{/* <Reviews venue = {venue}/> */}
</>
);
};
export default Venue;
Second option would be to use something like redux-persist so your data remains when the reload happens
const venues = useSelector((state) => state.venues)
render(){
<React.Fragment>
{
(venues && venues.venues && venues.venues instanceof Array && venues.venues.length>0) && venues.venues.map((elem,index)=>{
return(
<div className="venue-page-main" key={index}>
<h2>{elem.name}</h2>
<img src={elem.photo} alt="venue" />
</div>
);
})
}
</React.Fragment>
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>
I've created a common component and exported it, i need to call that component in action based on the result from API. If the api success that alert message component will call with a message as "updated successfully". error then show with an error message.
calling service method in action. is there any way we can do like this? is it possible to call a component in action
You have many options.
1. Redux
If you are a fan of Redux, or your project already use Redux, you might want to do it like this.
First declare the slice, provider and hook
const CommonAlertSlice = createSlice({
name: 'CommonAlert',
initialState : {
error: undefined
},
reducers: {
setError(state, action: PayloadAction<string>) {
state.error = action.payload;
},
clearError(state) {
state.error = undefined;
},
}
});
export const CommonAlertProvider: React.FC = ({children}) => {
const error = useSelector(state => state['CommonAlert'].error);
const dispatch = useDispatch();
return <>
<MyAlert
visible={error !== undefined}
body={error} onDismiss={() =>
dispatch(CommonAlertSlice.actions.clearError())} />
{children}
</>
}
export const useCommonAlert = () => {
const dispatch = useDispatch();
return {
setError: (error: string) => dispatch(CommonAlertSlice.actions.setError(error)),
}
}
And then use it like this.
const App: React.FC = () => {
return <CommonAlertProvider>
<YourComponent />
</CommonAlertProvider>
}
const YourComponent: React.FC = () => {
const { setError } = useCommonAlert();
useEffect(() => {
callYourApi()
.then(...)
.catch(err => {
setError(err.message);
});
});
return <> ... </>
}
2. React Context
If you like the built-in React Context, you can make it more simpler like this.
const CommonAlertContext = createContext({
setError: (error: string) => {}
});
export const CommonAlertProvider: React.FC = ({children}) => {
const [error, setError] = useState<string>();
return <CommonAlertContext.Provider value={{
setError
}}>
<MyAlert
visible={error !== undefined}
body={error} onDismiss={() => setError(undefined)} />
{children}
</CommonAlertContext.Provider>
}
export const useCommonAlert = () => useContext(CommonAlertContext);
And then use it the exact same way as in the Redux example.
3. A Hook Providing a Render Method
This option is the simplest.
export const useAlert = () => {
const [error, setError] = useState<string>();
return {
setError,
renderAlert: () => {
return <MyAlert
visible={error !== undefined}
body={error} onDismiss={() => setError(undefined)} />
}
}
}
Use it.
const YourComponent: React.FC = () => {
const { setError, renderAlert } = useAlert();
useEffect(() => {
callYourApi()
.then(...)
.catch(err => {
setError(err.message);
});
});
return <>
{renderAlert()}
...
</>
}
I saw the similar solution in Antd library, it was implemented like that
codesandbox link
App.js
import "./styles.css";
import alert from "./alert";
export default function App() {
const handleClick = () => {
alert();
};
return (
<div className="App">
<button onClick={handleClick}>Show alert</button>
</div>
);
}
alert function
import ReactDOM from "react-dom";
import { rootElement } from ".";
import Modal from "./Modal";
export default function alert() {
const modalEl = document.createElement("div");
rootElement.appendChild(modalEl);
function destroy() {
rootElement.removeChild(modalEl);
}
function render() {
ReactDOM.render(<Modal destroy={destroy} />, modalEl);
}
render();
}
Your modal component
import { useEffect } from "react";
export default function Modal({ destroy }) {
useEffect(() => {
return () => {
destroy();
};
}, [destroy]);
return (
<div>
Your alert <button onClick={destroy}>Close</button>
</div>
);
}
You can't call a Component in action, but you can use state for call a Component in render, using conditional rendering or state of Alert Component such as isShow.
I am displaying a foto in the front using Leigh Halliday's Image Previews in React with FileReader from https://www.youtube.com/watch?v=BPUgM1Ig4Po and everything is super BUT:
1.I want to get information from the image is displaying, exactly the base64 info, and have it then globally in my reactjs app.
2.for that reason I made a Context, i configured it ok BUT:
when I am doing dispatch inside a useEffect I want the image rendering and the info store in my variable globally
but I have one thing or another
if my image renders ok in my front, I can not obtain the value of my dispatch and viceversa
this is the code of my component:
import React, { useContext, useEffect, useRef, useState } from 'react'
import { AuthContext } from '../../auth/AuthContext'
import { types } from '../../types/types'
export const ButtonLoadFoto = () => {
const { dispatchFoto } = useContext(AuthContext)
const [image, setImage] = useState('')
const [preview, setPreview] = useState('')
const [status, setStatus] = useState(false)
useEffect(() => {
if (image) {
const reader = new FileReader()
reader.onloadend = () => {
setPreview(reader.result)
}
reader.readAsDataURL(image)
setStatus(true)
} else {
setPreview('')
}
}, [image])
// useEffect(() => {
// if (status) {
// dispatchFoto({
// type: types.foto,
// payload: {
// foto: preview.split(',')[1]
// }
// })
// }
// return () => setStatus(false)
// }, [preview])
const fileInputRef = useRef()
const handleRef = (e) => {
e.preventDefault()
fileInputRef.current.click()
}
const handleFile = (e) => {
const file = e.target.files[0]
if (file && file.type.substr(0, 5) === 'image') {
setImage(file)
}
}
return (
<div className='load-input '>
{
preview
?
(<img src={preview} alt='' onClick={() => setImage('')} />)
:
(<button
className='alert alert-danger'
onClick={handleRef}>
foto
</button>
)
}
< input
type='file'
style={{ display: 'none' }}
ref={fileInputRef}
accept='image/*'
onChange={handleFile}
/>
</div>
)
}
in the code above if you put away the comments we will have the information we want but the foto won t display at all
thanks all for your time , I really appreciate!
EDIT
this is the main component
import React, { useEffect, useReducer } from 'react'
import { AuthContext } from './auth/AuthContext'
import { fotoReducer } from './components/formScreen/fotoReducer'
import { AppRouter } from './routers/AppRouter'
const initImage = () => {
return { foto: '' }
}
export const CMI = () => {
const [foto, dispatchFoto] = useReducer(fotoReducer, {}, initImage)
return (
<div>
<AuthContext.Provider value={{
foto,
dispatchFoto
}}>
<AppRouter />
</AuthContext.Provider>
</div>
)
}
this is the componenent I use
import React, { useContext} from 'react'
import { ButtonLoadFoto } from '../components/formScreen/ButtonLoadFoto'
import { AuthContext } from '../auth/AuthContext'
export const FormScreen = () => {
const { foto } = useContext(AuthContext)
}
return (
<div>
<ButtonLoadFoto/>
</div>
)
as I said : if a render the image I can not have the information and viceversa...
when I use dispatch I don t know I it brokes the image render
thanks in advance
I have an issue where when my user is a new user both my create profile and create characters to FireBase Realtime database are not loading before my user profile page renders. I understand that useEffects run after the render. But after user profile and characters are created in the database I don't have the issue. I can log off and refresh my app, sign in and everything loads in time. Here is m code. I've tried writing my functions inside the useEffect several different ways and I get the same results every time. I saw one post where someone using a .then() but that doesn't appear to work in my situation. I rather not use any additional add-ins like AXIOs or other packages. I feel like there has to be a way to do this with the native built in tools of React and Firebase. Any advice is much appreciated. Edit: Here is my layout.
App.js
<AuthProvider>
<DBProvider>
<Switch>
<PrivateRoute path="/profile" component={ProfileBar} />
<PrivateRoute path="/update-profile" component={UpdateProfile} />
<Route path="/login" component={Login} />
<Route path="/signup" component={Signup} />
<Route path="/forgot-password" component={ForgotPassword} />
</Switch>
</DBProvider>
</AuthProvider>
</Router>
AuthContext.js
import React, { useContext, useState, useEffect } from 'react'
import { auth} from '../firebase'
const AuthContext = React.createContext()
export function useAuth() {
return useContext(AuthContext)
}
export function AuthProvider({ children }) {
const [currentUser, setCurrentUser] = useState()
const [loading, setLoading] = useState(true)
function signup(email, password, displayName) {
let promise = new Promise ((resolve, reject) => {
auth.createUserWithEmailAndPassword(email, password)
.then((ref) => {
ref.user.updateProfile({
displayName: displayName
});
resolve(ref);
})
.catch((error) => reject(error));
})
return promise
}
useEffect(() => {
const unsubscribe = auth.onAuthStateChanged(user => {
setCurrentUser(user)
setLoading(false)
})
return unsubscribe
}, [])
}
DBContext.js
import { db } from '../firebase'
import { useAuth } from './AuthContext'
import React, { useState, useEffect, useContext } from 'react'
const DBContext = React.createContext() // React Database FireStore .DB
export function useDB() {
useContext(DBContext);
}
export function DBProvider({ children }) {
const [profileData, setProfileData] = useState()
const [loading, setLoading] = useState(true)
const { currentUser } = useAuth()
function checkCurrentUser(){
if(currentUser){
checkProfile()
}
if(!currentUser){
setLoading(false)
console.log("No current user logged in.")
}
}
function checkProfile(){
db.ref(`users/` + currentUser.uid + `/profile`)
.on('value', (snapshot) => {
const data = snapshot.val()
if(data == null){
console.log(data, "New user... Generating profile!")
createUserProfile()
}
if(data){
getProfile()
}
});
}
function createUserProfile(){
let profile = {}
profile.gameMaster = false
profile.editor = false
profile.email = currentUser.email
profile.displayName = currentUser.displayName
db.ref('users/' + currentUser.uid).set({
profile
}).then(() =>{
getProfile()
})
}
function getProfile(){
db.ref(`users/` + currentUser.uid + `/profile`)
.on('value', (snapshot) => {
const profile = snapshot.val()
setLoading(false)
setProfileData(profile)
console.log("Profile set to State from Database.")
})
}
useEffect(() => {
checkCurrentUser()
},[])
}
Profile.js
<Switch>
<CharacterProvider>
<Route path={`${match.path}/characters`} component={CharacterSheets} />
<Route path={`${match.path}/journal`} component={Journal} />
<Route path={`${match.path}/game_charts`} component={GameCharts} />
<Route path={`${match.path}/game_rules`} component={GameRules} />
</CharacterProvider>
</Switch>
CharacterContext.js
useEffect(() => {
const ref = db.ref(`users/` + currentUser.uid + `/characters`)
ref.on('value', snapshot => {
const data = snapshot.val()
if(data){
console.log("Setting Characters to State from Database.")
setCharacters(JSON.parse(data))
setLoading(false)
}
if(data == null){
console.log("Setting Characters to State from template.")
setCharacters(characterTemplate)
setLoading(false)
}
})
return () => ref.off();
}, [])
useEffect(() => {
if(characters){
db.ref(`users/` + currentUser.uid).child("/characters").set(JSON.stringify(characters))
}
console.log("Data saved to firebase.")
}, [characters])
CharacterCards.js
import { useCharacter } from '../../../contexts/CharacterContext'
import CharacterCard from './CharacterCard'
import CharacterCardEdit from '../../ProfileContainer/CharacterEdit/CharacterCardEdit'
import SuccessRoller from '../CharacterComponents/SuccessRoller/SuccessRoller'
export default function CharacterCards() {
const { handleCharacterAdd, characters, selectedCharacter, selectedCharacterSuccessRoller } = useCharacter()
return (
<div>
<div className="add_button-container">
<button onClick={handleCharacterAdd} className="add_button-main" >Add Character</button>
</div>
<div className="parent-container">
<div>
{characters?.map(character => {
return (
<CharacterCard key={character.id} {...character} />
)
})
}
</div>
<div>
{selectedCharacter && <CharacterCardEdit character={selectedCharacter} />}
{selectedCharacterSuccessRoller && <SuccessRoller character={selectedCharacterSuccessRoller} />}
</div>
</div>
</div>
)
}
Because your code is sharded out into many functions for readability, there are a lot of listeners that are created but don't get cleaned up. In particular great care needs to be taken with .on listeners as they may be re-fired (you could use .once() to help with this). An example of this bug is in checkProfile() which listens to the user's profile, then calls getProfile() which also listens to the profile. Each time the profile is added, another call to getProfile() is made, adding yet another listener. Plus, each of the listeners in checkProfile() and getProfile() aren't ever cleaned up.
I've made a number of assumptions about your code structure and untangled it so you can read and understand it top-to-bottom. This is especially important when working with React hooks as their order matters.
// firebase.js
import firebase from "firebase/app";
import "firebase/auth";
import "firebase/database";
firebase.initializeApp({ /* ... */ });
const auth = firebase.auth();
const db = firebase.database();
export {
firebase,
auth,
db
}
// AuthContext.js
import { auth } from "./firebase";
import React, { useContext, useEffect, useState } from "react";
const AuthContext = React.createContext();
export default AuthContext;
export function useAuth() { // <- this is an assumption
return useContext(AuthContext);
}
async function signup(email, password, avatarName) {
const userCredential = await auth.createUserWithEmailAndPassword(email, password);
await userCredential.user.updateProfile({
displayName: avatarName
});
return userCredential;
}
export function AuthProvider(props) {
const [currentUser, setCurrentUser] = useState();
const [loading, setLoading] = useState(true);
useEffect(() => auth.onAuthStateChanged(user => {
setCurrentUser(user)
setLoading(false)
}), []);
return (
<AuthContext.Provider
value={{
currentUser,
loading,
signup
}}
>
{props.children}
</AuthContext.Provider>
);
}
// DBContext.js
import { db } from "./firebase";
import { useAuth } from "./AuthContext";
import React, { useEffect, useState } from "react";
const DBContext = React.createContext();
export default DBContext;
export function DBProvider(props) {
const [profileData, setProfileData] = useState();
const [loading, setLoading] = useState(true);
const { currentUser, loading: loadingUser } = useAuth();
useEffect(() => {
if (loadingUser) {
return; // still initializing, do nothing.
}
if (currentUser === null) {
// no user signed in!
setProfileData(null);
return;
}
// user is logged in
const profileRef = db.ref(`users/` + currentUser.uid + `/profile`);
const listener = profileRef.on('value', snapshot => {
if (!snapshot.exists()) {
// didn't find a profile for this user
snapshot.ref
.set({ // <- this will refire this listener (if successful) with the below data
gameMaster: false,
editor: false,
email: currentUser.email,
displayName: currentUser.displayName
})
.catch((error) => console.error("Failed to initialize default profile", error));
return;
}
setProfileData(snapshot.val());
setLoading(false);
});
return () => profileRef.off('value', listener); // <- cleans up listener
}, [currentUser, loadingUser]);
return (
<DBContext.Provider
value={{
profileData,
loading
}}
>
{props.children}
</DBContext.Provider>
);
}
// CharacterContext.js
import { db } from "./firebase";
import { useAuth } from "./AuthContext";
import React, { useEffect, useState } from "react";
const CharacterContext = React.createContext();
export default CharacterContext;
export function CharacterProvider(props) {
const { currentUser, loading: loadingUser } = useAuth();
const [characters, setCharacters] = useState();
const [loading, setLoading] = useState(true);
useEffect(() => {
if (loadingUser) {
return; // still initializing, do nothing.
}
if (!currentUser) {
// no user signed in!
setCharacters(null);
return;
}
const charactersRef = db.ref(`users/${currentUser.uid}/characters`);
const listener = charactersRef.on('value', snapshot => {
if (!snapshot.exists()) {
// no character data found, create from template
snapshot.ref
.set(DEFAULT_CHARACTERS); // <- this will refire this listener (if successful)
.catch((error) => console.error("Failed to initialize default characters", error));
return;
}
setCharacters(JSON.parse(snapshot.val()));
setLoading(false);
});
return () => charactersRef.off('value', listener);
}, [currentUser, loadingUser]);
return (
<CharacterContext.Provider
value={{
characters,
loading
}}
>
{props.children}
</CharacterContext.Provider>
);
}
I thought had a better grasp of hooks but I've clearly got something wrong here. Not all of the character objects will have what I'm trying to get but it wont work with those that do.
I cna't even build in a check for character.comics.available. Same errors appear. I'm presuming I'm getting them before the state is set? But {character.name} always works.
CharacterDetail.js
import React from "react";
import { useParams } from "react-router-dom";
import useCharacter from "../hooks/useCharacter";
const CharacterDetail = () => {
// from the Route path="/character/:id"
const { id } = useParams();
// custom hook. useCharacter.js
const [character] = useCharacter(id);
// this only works sometimes but errors if i refresh the page
// console.log(character.comics.available);
return (
<div>
<h2 className="ui header">Character Details</h2>
<p>Works every time: {character.name}</p>
<div className="ui segment"></div>
<pre></pre>
</div>
);
};
export default CharacterDetail;
Custom hook useCharacter.js
import { useState, useEffect } from "react";
import marvel from "../apis/marvel";
const useCharacter = (id) => {
const [character, setCharacter] = useState({});
useEffect(() => {
loadItem();
return () => {};
}, [id]);
const loadItem = async (term) => {
const response = await marvel.get(`/characters/${id}`);
console.log(response.data.data.results[0]);
setCharacter(response.data.data.results[0]);
};
return [character];
};
export default useCharacter;
error when console is uncommented
Uncaught TypeError: Cannot read property 'available' of undefined
at CharacterDetail (CharacterDetail.js:11)
...
Here is the character object.
thanks to #Nikita for the pointers. Settled on this...
CharacterDetail.js
import React from "react";
import { useParams } from "react-router-dom";
import useCharacter from "../hooks/useCharacter";
const CharacterDetail = () => {
const { id } = useParams();
// custom hook. useCharacter.js
const { isLoading, character } = useCharacter(id);
const isArray = character instanceof Array;
if (!isLoading && isArray === false) {
console.log("isLoading", isArray);
const thumb =
character.thumbnail.path +
"/portrait_uncanny." +
character.thumbnail.extension;
return (
<div>
<h2 className="ui header">{character.name}</h2>
<img src={thumb} />
<div className="ui segment">{character.comics.available}</div>
<div className="ui segment">{character.series.available}</div>
<div className="ui segment">{character.stories.available}</div>
</div>
);
}
return <div>Loading...</div>;
};
export default CharacterDetail;
useCharacter.js
import { useState, useEffect } from "react";
import marvel from "../apis/marvel";
function useCharacter(id) {
const [character, setCharacter] = useState([]);
const [isLoading, setIsLoading] = useState(false);
useEffect(() => {
setIsLoading(true);
const fetchData = async () => {
setIsLoading(true);
await marvel
.get(`/characters/${id}`)
.then((response) => {
/* DO STUFF WHEN THE CALLS SUCCEEDS */
setIsLoading(false);
setCharacter(response.data.data.results[0]);
})
.catch((e) => {
/* HANDLE THE ERROR (e) */
});
};
fetchData();
}, [id]);
return {
isLoading,
character,
};
}
export default useCharacter;